iOS remote client: native UIKit surface, WS logging, client-side layout persistence
- Add remote-client-ios: full native UIKit app with Objects/View/UILayout/Functions pattern - Widget hierarchy: BaseWidget → Fader, Toggle, Transport, Title, Feedback, LogWidget - LoggingVC: child VC embedded in LogWidget for live WS message inspection - Codable model: ModelPresetLoadToRemoteDevice with snake_case decoder strategy - Layout persistence: UserDefaults keyed by preset_uuid, fully client-side - WS logging: dedicated WS In/Out floating panels on desktop app - Remove HTTP server and JS client (replaced by native iOS app) - Add Xcode noise to .gitignore (xcuserstate, xcuserdata) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
979A62DE2FF478D6002D3E46 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 979A62BF2FF478D4002D3E46 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 979A62C62FF478D4002D3E46;
|
||||
remoteInfo = "remote-client-ios";
|
||||
};
|
||||
979A62E82FF478D6002D3E46 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 979A62BF2FF478D4002D3E46 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 979A62C62FF478D4002D3E46;
|
||||
remoteInfo = "remote-client-ios";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
979A62C72FF478D4002D3E46 /* remote-client-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "remote-client-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "remote-client-iosTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "remote-client-iosUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
979A62EF2FF478D6002D3E46 /* Exceptions for "remote-client-ios" folder in "remote-client-ios" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 979A62C62FF478D4002D3E46 /* remote-client-ios */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
979A62C92FF478D4002D3E46 /* remote-client-ios */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
979A62EF2FF478D6002D3E46 /* Exceptions for "remote-client-ios" folder in "remote-client-ios" target */,
|
||||
);
|
||||
path = "remote-client-ios";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
979A62E02FF478D6002D3E46 /* remote-client-iosTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "remote-client-iosTests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "remote-client-iosUITests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
979A62C42FF478D4002D3E46 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
979A62DA2FF478D6002D3E46 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
979A62E42FF478D6002D3E46 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
979A62BE2FF478D4002D3E46 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
979A62C92FF478D4002D3E46 /* remote-client-ios */,
|
||||
979A62E02FF478D6002D3E46 /* remote-client-iosTests */,
|
||||
979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */,
|
||||
979A62C82FF478D4002D3E46 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
979A62C82FF478D4002D3E46 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
979A62C72FF478D4002D3E46 /* remote-client-ios.app */,
|
||||
979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */,
|
||||
979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
979A62C62FF478D4002D3E46 /* remote-client-ios */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 979A62F02FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-ios" */;
|
||||
buildPhases = (
|
||||
979A62C32FF478D4002D3E46 /* Sources */,
|
||||
979A62C42FF478D4002D3E46 /* Frameworks */,
|
||||
979A62C52FF478D4002D3E46 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
979A62C92FF478D4002D3E46 /* remote-client-ios */,
|
||||
);
|
||||
name = "remote-client-ios";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "remote-client-ios";
|
||||
productReference = 979A62C72FF478D4002D3E46 /* remote-client-ios.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
979A62DC2FF478D6002D3E46 /* remote-client-iosTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 979A62F52FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosTests" */;
|
||||
buildPhases = (
|
||||
979A62D92FF478D6002D3E46 /* Sources */,
|
||||
979A62DA2FF478D6002D3E46 /* Frameworks */,
|
||||
979A62DB2FF478D6002D3E46 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
979A62DF2FF478D6002D3E46 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
979A62E02FF478D6002D3E46 /* remote-client-iosTests */,
|
||||
);
|
||||
name = "remote-client-iosTests";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "remote-client-iosTests";
|
||||
productReference = 979A62DD2FF478D6002D3E46 /* remote-client-iosTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
979A62E62FF478D6002D3E46 /* remote-client-iosUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 979A62F82FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosUITests" */;
|
||||
buildPhases = (
|
||||
979A62E32FF478D6002D3E46 /* Sources */,
|
||||
979A62E42FF478D6002D3E46 /* Frameworks */,
|
||||
979A62E52FF478D6002D3E46 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
979A62E92FF478D6002D3E46 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
979A62EA2FF478D6002D3E46 /* remote-client-iosUITests */,
|
||||
);
|
||||
name = "remote-client-iosUITests";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "remote-client-iosUITests";
|
||||
productReference = 979A62E72FF478D6002D3E46 /* remote-client-iosUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
979A62BF2FF478D4002D3E46 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1640;
|
||||
LastUpgradeCheck = 1640;
|
||||
TargetAttributes = {
|
||||
979A62C62FF478D4002D3E46 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
};
|
||||
979A62DC2FF478D6002D3E46 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
TestTargetID = 979A62C62FF478D4002D3E46;
|
||||
};
|
||||
979A62E62FF478D6002D3E46 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
TestTargetID = 979A62C62FF478D4002D3E46;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 979A62C22FF478D4002D3E46 /* Build configuration list for PBXProject "remote-client-ios" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 979A62BE2FF478D4002D3E46;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 979A62C82FF478D4002D3E46 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
979A62C62FF478D4002D3E46 /* remote-client-ios */,
|
||||
979A62DC2FF478D6002D3E46 /* remote-client-iosTests */,
|
||||
979A62E62FF478D6002D3E46 /* remote-client-iosUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
979A62C52FF478D4002D3E46 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
979A62DB2FF478D6002D3E46 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
979A62E52FF478D6002D3E46 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
979A62C32FF478D4002D3E46 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
979A62D92FF478D6002D3E46 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
979A62E32FF478D6002D3E46 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
979A62DF2FF478D6002D3E46 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 979A62C62FF478D4002D3E46 /* remote-client-ios */;
|
||||
targetProxy = 979A62DE2FF478D6002D3E46 /* PBXContainerItemProxy */;
|
||||
};
|
||||
979A62E92FF478D6002D3E46 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 979A62C62FF478D4002D3E46 /* remote-client-ios */;
|
||||
targetProxy = 979A62E82FF478D6002D3E46 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
979A62F12FF478D6002D3E46 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "remote-client-ios/Info.plist";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-ios";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
979A62F22FF478D6002D3E46 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "remote-client-ios/Info.plist";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-ios";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
979A62F32FF478D6002D3E46 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
979A62F42FF478D6002D3E46 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
979A62F62FF478D6002D3E46 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/remote-client-ios.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/remote-client-ios";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
979A62F72FF478D6002D3E46 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/remote-client-ios.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/remote-client-ios";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
979A62F92FF478D6002D3E46 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosUITests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = "remote-client-ios";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
979A62FA2FF478D6002D3E46 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = LTQ6N8Q7YG;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.japan4.remote-client-iosUITests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = "remote-client-ios";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
979A62C22FF478D4002D3E46 /* Build configuration list for PBXProject "remote-client-ios" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
979A62F32FF478D6002D3E46 /* Debug */,
|
||||
979A62F42FF478D6002D3E46 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
979A62F02FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-ios" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
979A62F12FF478D6002D3E46 /* Debug */,
|
||||
979A62F22FF478D6002D3E46 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
979A62F52FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
979A62F62FF478D6002D3E46 /* Debug */,
|
||||
979A62F72FF478D6002D3E46 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
979A62F82FF478D6002D3E46 /* Build configuration list for PBXNativeTarget "remote-client-iosUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
979A62F92FF478D6002D3E46 /* Debug */,
|
||||
979A62FA2FF478D6002D3E46 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 979A62BF2FF478D4002D3E46 /* Project object */;
|
||||
}
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// remote-client-ios
|
||||
//
|
||||
// Created by p4piwabl0 on 6/30/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,283 @@
|
||||
import UIKit
|
||||
|
||||
extension ObjectsViewController {
|
||||
|
||||
// MARK: - Connection State UI
|
||||
func applyConnectionState(_ state: ConnectionState) {
|
||||
self.connectionState = state
|
||||
let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||
let red = UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1)
|
||||
let gray = UIColor(white: 0.3, alpha: 1)
|
||||
|
||||
switch state {
|
||||
case .notStarted:
|
||||
self.statusLabel.text = "not started"
|
||||
self.connectButton.setTitle("Connect", for: .normal)
|
||||
self.connectButton.backgroundColor = green
|
||||
self.connectButton.setTitleColor(.black, for: .normal)
|
||||
self.connectButton.isEnabled = true
|
||||
case .connecting:
|
||||
self.statusLabel.text = "connecting..."
|
||||
self.connectButton.setTitle("Connecting...", for: .normal)
|
||||
self.connectButton.backgroundColor = gray
|
||||
self.connectButton.setTitleColor(.lightGray, for: .normal)
|
||||
self.connectButton.isEnabled = false
|
||||
case .connected:
|
||||
self.statusLabel.text = "connected"
|
||||
self.connectButton.setTitle("Disconnect", for: .normal)
|
||||
self.connectButton.backgroundColor = red
|
||||
self.connectButton.setTitleColor(.white, for: .normal)
|
||||
self.connectButton.isEnabled = true
|
||||
case .disconnected:
|
||||
self.statusLabel.text = "disconnected"
|
||||
self.connectButton.setTitle("Reconnect", for: .normal)
|
||||
self.connectButton.backgroundColor = green
|
||||
self.connectButton.setTitleColor(.black, for: .normal)
|
||||
self.connectButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WebSocket Connect / Disconnect
|
||||
@objc func connectTapped() {
|
||||
if self.connectionState == .connected {
|
||||
self.wsTask?.cancel(with: .goingAway, reason: nil)
|
||||
self.wsTask = nil
|
||||
self.applyConnectionState(.disconnected)
|
||||
} else {
|
||||
self.connectWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
func connectWebSocket() {
|
||||
let ip = self.ipField.text?.trimmingCharacters(in: .whitespaces) ?? "127.0.0.1"
|
||||
let port = self.portField.text?.trimmingCharacters(in: .whitespaces) ?? "8765"
|
||||
guard let url = URL(string: "ws://\(ip):\(port)") else { return }
|
||||
self.urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
|
||||
self.wsTask = self.urlSession?.webSocketTask(with: url)
|
||||
self.wsTask?.resume()
|
||||
self.applyConnectionState(.connecting)
|
||||
self.listen()
|
||||
}
|
||||
|
||||
// MARK: - Listen
|
||||
func listen() {
|
||||
self.wsTask?.receive { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .success(let message):
|
||||
switch message {
|
||||
case .string(let text): self.handleMessage(text)
|
||||
default: break
|
||||
}
|
||||
self.listen()
|
||||
case .failure:
|
||||
DispatchQueue.main.async { self.applyConnectionState(.disconnected) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Show Logging Toggle
|
||||
@objc func showLoggingTapped() {
|
||||
let nowVisible = self.logWidget.isHidden
|
||||
self.logWidget.isHidden = !nowVisible
|
||||
self.showLoggingButton.setTitle(nowVisible ? "Hide Logging" : "Show Logging", for: .normal)
|
||||
}
|
||||
|
||||
// MARK: - Handle Incoming Message
|
||||
func handleMessage(_ text: String) {
|
||||
self.loggingView.log(text)
|
||||
guard let data = text.data(using: .utf8),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let event = json["event"] as? String else { return }
|
||||
|
||||
switch event {
|
||||
case "preset":
|
||||
if let presetData = json["data"] as? [String: Any],
|
||||
let presetBytes = try? JSONSerialization.data(withJSONObject: presetData) {
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let preset = try decoder.decode(ModelPresetLoadToRemoteDevice.self, from: presetBytes)
|
||||
DispatchQueue.main.async { self.renderPreset(preset) }
|
||||
} catch {
|
||||
self.loggingView.log("decode error: \(error)")
|
||||
}
|
||||
}
|
||||
case "widget_update":
|
||||
if let uid = json["uid"] as? String, let value = json["value"] as? Int {
|
||||
DispatchQueue.main.async { self.updateWidget(uid: uid, value: value) }
|
||||
}
|
||||
case "daw_state":
|
||||
DispatchQueue.main.async { self.updateDawState(json) }
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Send
|
||||
func send(_ dict: [String: Any]) {
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: dict),
|
||||
let text = String(data: data, encoding: .utf8) else { return }
|
||||
self.wsTask?.send(.string(text)) { _ in }
|
||||
}
|
||||
|
||||
// MARK: - Render Preset
|
||||
func renderPreset(_ preset: ModelPresetLoadToRemoteDevice) {
|
||||
let savedLayout = UserDefaults.standard.dictionary(forKey: "layout_\(preset.presetUuid)") ?? [:]
|
||||
self.presetName = preset.presetName
|
||||
self.presetUuid = preset.presetUuid
|
||||
self.widgets = [:]
|
||||
self.feedbackWidgets = []
|
||||
self.containerView.subviews.forEach { if $0 !== self.logWidget { $0.removeFromSuperview() } }
|
||||
self.presetLabel.text = preset.presetName
|
||||
|
||||
let titleUid = "title-big"
|
||||
let titleFrame = savedFrame(uid: titleUid, in: savedLayout) ?? CGRect(x: 16, y: 16, width: 300, height: 44)
|
||||
let titleWidget = TitleWidget(uid: titleUid, text: preset.displayTitle, frame: titleFrame)
|
||||
self.setupDrag(on: titleWidget)
|
||||
self.containerView.addSubview(titleWidget)
|
||||
self.widgets[titleUid] = titleWidget
|
||||
|
||||
var faderIdx = 0
|
||||
var squareIdx = 0
|
||||
|
||||
for f in preset.faders where f.visible {
|
||||
let frame = savedFrame(uid: f.uid, in: savedLayout) ?? FaderWidget.defaultFrame(idx: faderIdx)
|
||||
let w = FaderWidget(uid: f.uid,
|
||||
label: f.label,
|
||||
value: f.value,
|
||||
color: UIColor.fromHex(f.color),
|
||||
frame: frame)
|
||||
w.onSend = { [weak self] uid, val in self?.send(["event": "control", "uid": uid, "value": val]) }
|
||||
self.setupDrag(on: w)
|
||||
self.containerView.addSubview(w)
|
||||
self.widgets[f.uid] = w
|
||||
faderIdx += 1
|
||||
}
|
||||
|
||||
for t in preset.toggles where t.visible {
|
||||
let frame = savedFrame(uid: t.uid, in: savedLayout) ?? ToggleWidget.defaultFrame(idx: squareIdx)
|
||||
let w = ToggleWidget(uid: t.uid,
|
||||
label: t.label,
|
||||
isOn: t.state,
|
||||
color: UIColor.fromHex(t.color),
|
||||
frame: frame)
|
||||
w.onSend = { [weak self] uid, val in self?.send(["event": "control", "uid": uid, "value": val]) }
|
||||
self.setupDrag(on: w)
|
||||
self.containerView.addSubview(w)
|
||||
self.widgets[t.uid] = w
|
||||
squareIdx += 1
|
||||
}
|
||||
|
||||
for t in preset.transports where t.visible {
|
||||
let frame = savedFrame(uid: t.uid, in: savedLayout) ?? TransportWidget.defaultFrame(idx: squareIdx)
|
||||
let w = TransportWidget(uid: t.uid,
|
||||
label: t.label,
|
||||
frame: frame)
|
||||
w.onSend = { [weak self] uid, val in self?.send(["event": "control", "uid": uid, "value": val]) }
|
||||
self.setupDrag(on: w)
|
||||
self.containerView.addSubview(w)
|
||||
self.widgets[t.uid] = w
|
||||
squareIdx += 1
|
||||
}
|
||||
|
||||
self.isLocked = !savedLayout.isEmpty
|
||||
self.refreshToolbar()
|
||||
}
|
||||
|
||||
// MARK: - Update Widget
|
||||
func updateWidget(uid: String, value: Int) {
|
||||
self.widgets[uid]?.update(value: value)
|
||||
}
|
||||
|
||||
// MARK: - DAW State
|
||||
func updateDawState(_ json: [String: Any]) {
|
||||
let track = json["track"] as? String
|
||||
let bar = json["bar"] as? Int
|
||||
let playing = json["playing"] as? Bool
|
||||
|
||||
self.feedbackWidgets.forEach { $0.updateDaw(track: track, bar: bar, playing: playing) }
|
||||
|
||||
var parts: [String] = []
|
||||
if let track = track { parts.append(track) }
|
||||
if let bar = bar { parts.append("Bar \(bar)") }
|
||||
if let playing = playing { parts.append(playing ? "▶" : "■") }
|
||||
if !parts.isEmpty { self.statusLabel.text = parts.joined(separator: " ") }
|
||||
}
|
||||
|
||||
// MARK: - Toolbar
|
||||
@objc func arrangeTapped() {
|
||||
self.isLocked = false
|
||||
self.refreshToolbar()
|
||||
}
|
||||
|
||||
@objc func lockTapped() {
|
||||
self.isLocked = true
|
||||
self.refreshToolbar()
|
||||
self.saveLayout()
|
||||
}
|
||||
|
||||
func refreshToolbar() {
|
||||
let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||
self.lockButton.backgroundColor = self.isLocked ? green : UIColor(white: 0.15, alpha: 1)
|
||||
self.lockButton.setTitleColor(self.isLocked ? .black : .lightGray, for: .normal)
|
||||
self.arrangeButton.backgroundColor = self.isLocked ? UIColor(white: 0.15, alpha: 1) : green
|
||||
self.arrangeButton.setTitleColor(self.isLocked ? .lightGray : .black, for: .normal)
|
||||
}
|
||||
|
||||
// MARK: - Save Layout
|
||||
func saveLayout() {
|
||||
guard !self.presetUuid.isEmpty else {
|
||||
self.loggingView.log("saveLayout: aborted — presetUuid is empty")
|
||||
return
|
||||
}
|
||||
var layoutDict: [String: Any] = [:]
|
||||
for (uid, w) in self.widgets {
|
||||
let f = w.frame
|
||||
layoutDict[uid] = [
|
||||
"x": Double(f.origin.x),
|
||||
"y": Double(f.origin.y),
|
||||
"w": Double(f.width),
|
||||
"h": Double(f.height)
|
||||
]
|
||||
}
|
||||
UserDefaults.standard.set(layoutDict, forKey: "layout_\(self.presetUuid)")
|
||||
self.loggingView.log("saveLayout: \(layoutDict.count) widgets saved locally for \(self.presetUuid)")
|
||||
}
|
||||
|
||||
// MARK: - Drag
|
||||
func setupDrag(on view: UIView) {
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||||
view.addGestureRecognizer(pan)
|
||||
}
|
||||
|
||||
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
guard !self.isLocked, let view = gesture.view else { return }
|
||||
let translation = gesture.translation(in: self.containerView)
|
||||
view.center = CGPoint(x: view.center.x + translation.x, y: view.center.y + translation.y)
|
||||
gesture.setTranslation(.zero, in: self.containerView)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
func savedFrame(uid: String, in savedLayout: [String: Any]) -> CGRect? {
|
||||
guard let saved = savedLayout[uid] as? [String: Any],
|
||||
let x = saved["x"] as? CGFloat, let y = saved["y"] as? CGFloat,
|
||||
let w = saved["w"] as? CGFloat, let h = saved["h"] as? CGFloat
|
||||
else { return nil }
|
||||
return CGRect(x: x, y: y, width: w, height: h)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - WebSocket Delegate
|
||||
extension ObjectsViewController: URLSessionWebSocketDelegate {
|
||||
|
||||
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
||||
self.applyConnectionState(.connected)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||
self.applyConnectionState(.disconnected)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import UIKit
|
||||
|
||||
extension LoggingObjects {
|
||||
|
||||
func log(_ message: String) {
|
||||
DispatchQueue.main.async {
|
||||
var display = message
|
||||
if let data = message.data(using: .utf8),
|
||||
let obj = try? JSONSerialization.jsonObject(with: data),
|
||||
let pretty = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted),
|
||||
let str = String(data: pretty, encoding: .utf8) {
|
||||
display = str
|
||||
}
|
||||
let current = self.textView.text ?? ""
|
||||
let separator = current.isEmpty ? "" : "\n--- ✦ ---\n"
|
||||
self.textView.text = display + separator + current
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearTapped() {
|
||||
self.textView.text = ""
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import UIKit
|
||||
|
||||
class LoggingObjects: UIViewController {
|
||||
|
||||
lazy var textView: UITextView = {
|
||||
let tv = UITextView()
|
||||
tv.translatesAutoresizingMaskIntoConstraints = false
|
||||
tv.backgroundColor = .clear
|
||||
tv.textColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||
tv.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
|
||||
tv.isEditable = false
|
||||
tv.isScrollEnabled = true
|
||||
return tv
|
||||
}()
|
||||
|
||||
lazy var clearButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.setTitle("Clear", for: .normal)
|
||||
btn.setTitleColor(UIColor(white: 0.4, alpha: 1), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 10)
|
||||
btn.addTarget(self, action: #selector(self.clearTapped), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import UIKit
|
||||
|
||||
extension LoggingObjects {
|
||||
|
||||
func setupUI() {
|
||||
DispatchQueue.main.async {
|
||||
self.view.backgroundColor = UIColor(white: 0.08, alpha: 1)
|
||||
|
||||
self.view.addSubview(self.clearButton)
|
||||
self.clearButton.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 6).isActive = true
|
||||
self.clearButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
||||
|
||||
self.view.addSubview(self.textView)
|
||||
self.textView.topAnchor.constraint(equalTo: self.clearButton.bottomAnchor, constant: 2).isActive = true
|
||||
self.textView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 4).isActive = true
|
||||
self.textView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -4).isActive = true
|
||||
self.textView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -4).isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import UIKit
|
||||
|
||||
class LoggingView: LoggingObjects {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
struct ModelPresetLoadToRemoteDevice: Codable {
|
||||
let presetUuid: String
|
||||
let presetName: String
|
||||
let bigTitle: String
|
||||
let faders: [ModelFaderPayload]
|
||||
let toggles: [ModelTogglePayload]
|
||||
let transports: [ModelTransportPayload]
|
||||
|
||||
var displayTitle: String {
|
||||
bigTitle.isEmpty ? "No Title" : bigTitle
|
||||
}
|
||||
}
|
||||
|
||||
struct ModelFaderPayload: Codable {
|
||||
let uid: String
|
||||
let label: String
|
||||
let value: Int
|
||||
let visible: Bool
|
||||
let color: String
|
||||
}
|
||||
|
||||
struct ModelTogglePayload: Codable {
|
||||
let uid: String
|
||||
let label: String
|
||||
let state: Bool
|
||||
let visible: Bool
|
||||
let color: String
|
||||
}
|
||||
|
||||
struct ModelTransportPayload: Codable {
|
||||
let uid: String
|
||||
let label: String
|
||||
let visible: Bool
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import UIKit
|
||||
|
||||
class ObjectsViewController: UIViewController {
|
||||
|
||||
// MARK: - WebSocket
|
||||
var wsTask: URLSessionWebSocketTask?
|
||||
var urlSession: URLSession?
|
||||
|
||||
// MARK: - Connection State
|
||||
enum ConnectionState {
|
||||
case notStarted, connecting, connected, disconnected
|
||||
}
|
||||
var connectionState: ConnectionState = .notStarted
|
||||
|
||||
// MARK: - State
|
||||
var presetName: String = ""
|
||||
var presetUuid: String = ""
|
||||
var isLocked: Bool = true
|
||||
|
||||
// MARK: - Widgets
|
||||
var widgets: [String: BaseWidget] = [:]
|
||||
var feedbackWidgets: [FeedbackWidget] = []
|
||||
|
||||
// MARK: - Layout
|
||||
var layout: [String: CGRect] = [:]
|
||||
|
||||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = UIColor(white: 0.1, alpha: 1)
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - Toolbar
|
||||
lazy var connectButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.setTitle("Connect", for: .normal)
|
||||
btn.setTitleColor(.black, for: .normal)
|
||||
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||
btn.layer.cornerRadius = 4
|
||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||
btn.addTarget(self, action: #selector(self.connectTapped), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var arrangeButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.setTitle("Arrange", for: .normal)
|
||||
btn.setTitleColor(.lightGray, for: .normal)
|
||||
btn.backgroundColor = UIColor(white: 0.15, alpha: 1)
|
||||
btn.layer.borderWidth = 1
|
||||
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
btn.layer.cornerRadius = 4
|
||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||
btn.addTarget(self, action: #selector(self.arrangeTapped), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var lockButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.setTitle("Lock / Save", for: .normal)
|
||||
btn.setTitleColor(.black, for: .normal)
|
||||
btn.backgroundColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
|
||||
btn.layer.cornerRadius = 4
|
||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||
btn.addTarget(self, action: #selector(self.lockTapped), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
// MARK: - Connection Fields
|
||||
let ipField: UITextField = {
|
||||
let tf = UITextField()
|
||||
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||
tf.text = "127.0.0.1"
|
||||
tf.textColor = .lightGray
|
||||
tf.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||
tf.backgroundColor = UIColor(white: 0.12, alpha: 1)
|
||||
tf.layer.borderWidth = 1
|
||||
tf.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
tf.layer.cornerRadius = 4
|
||||
tf.keyboardType = .decimalPad
|
||||
tf.textAlignment = .center
|
||||
tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||
tf.leftViewMode = .always
|
||||
tf.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||
tf.rightViewMode = .always
|
||||
return tf
|
||||
}()
|
||||
|
||||
let portField: UITextField = {
|
||||
let tf = UITextField()
|
||||
tf.translatesAutoresizingMaskIntoConstraints = false
|
||||
tf.text = "8765"
|
||||
tf.textColor = .lightGray
|
||||
tf.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||
tf.backgroundColor = UIColor(white: 0.12, alpha: 1)
|
||||
tf.layer.borderWidth = 1
|
||||
tf.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
tf.layer.cornerRadius = 4
|
||||
tf.keyboardType = .numberPad
|
||||
tf.textAlignment = .center
|
||||
tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||
tf.leftViewMode = .always
|
||||
tf.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: 0))
|
||||
tf.rightViewMode = .always
|
||||
return tf
|
||||
}()
|
||||
|
||||
// MARK: - Logging
|
||||
lazy var loggingView: LoggingView = LoggingView()
|
||||
|
||||
lazy var logWidget: LogWidget = {
|
||||
let w = LogWidget(uid: "log-widget", frame: LogWidget.defaultFrame())
|
||||
return w
|
||||
}()
|
||||
|
||||
lazy var showLoggingButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.setTitle("Show Logging", for: .normal)
|
||||
btn.setTitleColor(.lightGray, for: .normal)
|
||||
btn.backgroundColor = UIColor(white: 0.15, alpha: 1)
|
||||
btn.layer.borderWidth = 1
|
||||
btn.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
btn.layer.cornerRadius = 4
|
||||
btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14)
|
||||
btn.addTarget(self, action: #selector(self.showLoggingTapped), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
// MARK: - Labels
|
||||
let statusLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.text = "not started"
|
||||
lbl.textColor = UIColor(white: 0.33, alpha: 1)
|
||||
lbl.font = .systemFont(ofSize: 11)
|
||||
return lbl
|
||||
}()
|
||||
|
||||
let presetLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.text = ""
|
||||
lbl.textColor = UIColor(white: 0.33, alpha: 1)
|
||||
lbl.font = .systemFont(ofSize: 11)
|
||||
return lbl
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import UIKit
|
||||
|
||||
extension ObjectsViewController {
|
||||
|
||||
func setupUI() {
|
||||
DispatchQueue.main.async {
|
||||
self.view.backgroundColor = UIColor(white: 0.1, alpha: 1)
|
||||
|
||||
// containerView
|
||||
self.view.addSubview(self.containerView)
|
||||
self.containerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
|
||||
self.containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
|
||||
self.containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
|
||||
self.containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
|
||||
|
||||
// statusLabel
|
||||
self.view.addSubview(self.statusLabel)
|
||||
self.statusLabel.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.statusLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
||||
|
||||
// presetLabel
|
||||
self.view.addSubview(self.presetLabel)
|
||||
self.presetLabel.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -8).isActive = true
|
||||
self.presetLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8).isActive = true
|
||||
|
||||
// toolbar buttons (top-right)
|
||||
self.view.addSubview(self.showLoggingButton)
|
||||
self.showLoggingButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.showLoggingButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
|
||||
|
||||
self.view.addSubview(self.connectButton)
|
||||
self.connectButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.connectButton.trailingAnchor.constraint(equalTo: self.showLoggingButton.leadingAnchor, constant: -6).isActive = true
|
||||
|
||||
self.view.addSubview(self.portField)
|
||||
self.portField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.portField.trailingAnchor.constraint(equalTo: self.connectButton.leadingAnchor, constant: -6).isActive = true
|
||||
self.portField.widthAnchor.constraint(equalToConstant: 60).isActive = true
|
||||
self.portField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
||||
|
||||
self.view.addSubview(self.ipField)
|
||||
self.ipField.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.ipField.trailingAnchor.constraint(equalTo: self.portField.leadingAnchor, constant: -6).isActive = true
|
||||
self.ipField.widthAnchor.constraint(equalToConstant: 120).isActive = true
|
||||
self.ipField.heightAnchor.constraint(equalTo: self.connectButton.heightAnchor).isActive = true
|
||||
|
||||
self.view.addSubview(self.lockButton)
|
||||
self.lockButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.lockButton.trailingAnchor.constraint(equalTo: self.ipField.leadingAnchor, constant: -6).isActive = true
|
||||
|
||||
self.view.addSubview(self.arrangeButton)
|
||||
self.arrangeButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
||||
self.arrangeButton.trailingAnchor.constraint(equalTo: self.lockButton.leadingAnchor, constant: -6).isActive = true
|
||||
|
||||
self.containerView.addSubview(self.logWidget)
|
||||
self.setupDrag(on: self.logWidget)
|
||||
self.logWidget.attach(to: self, loggingView: self.loggingView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// remote-client-ios
|
||||
//
|
||||
// Created by p4piwabl0 on 6/30/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ViewController: ObjectsViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupUI()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import UIKit
|
||||
|
||||
class BaseWidget: UIView {
|
||||
|
||||
let uid: String
|
||||
var onSend: ((String, Int) -> Void)?
|
||||
|
||||
init(uid: String, frame: CGRect) {
|
||||
self.uid = uid
|
||||
super.init(frame: frame)
|
||||
self.backgroundColor = UIColor(white: 0.13, alpha: 1)
|
||||
self.layer.borderWidth = 1
|
||||
self.layer.cornerRadius = 6
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func update(value: Int) {}
|
||||
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
static func fromHex(_ hex: String) -> UIColor {
|
||||
var h = hex.trimmingCharacters(in: .alphanumerics.inverted)
|
||||
if h.count == 6 { h = "FF" + h }
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: h).scanHexInt64(&int)
|
||||
return UIColor(
|
||||
red: CGFloat((int >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((int >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(int & 0xFF) / 255,
|
||||
alpha: CGFloat((int >> 24) & 0xFF) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import UIKit
|
||||
|
||||
class FaderWidget: BaseWidget {
|
||||
|
||||
static let size = CGSize(width: 140, height: 360)
|
||||
|
||||
static func defaultFrame(idx: Int) -> CGRect {
|
||||
let col = idx % 3
|
||||
let row = idx / 3
|
||||
return CGRect(x: 16 + CGFloat(col) * 170, y: 48 + CGFloat(row) * 380, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
let color: UIColor
|
||||
private var sliderWidthConstraint: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Subviews
|
||||
lazy var nameLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.textColor = UIColor(white: 0.53, alpha: 1)
|
||||
lbl.font = .systemFont(ofSize: 14)
|
||||
lbl.textAlignment = .center
|
||||
return lbl
|
||||
}()
|
||||
|
||||
lazy var slider: UISlider = {
|
||||
let s = UISlider()
|
||||
s.translatesAutoresizingMaskIntoConstraints = false
|
||||
s.minimumValue = 0
|
||||
s.maximumValue = 127
|
||||
s.transform = CGAffineTransform(rotationAngle: -.pi / 2)
|
||||
return s
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
init(uid: String, label: String, value: Int, color: UIColor, frame: CGRect) {
|
||||
self.color = color
|
||||
super.init(uid: uid, frame: frame)
|
||||
self.layer.borderColor = color.cgColor
|
||||
self.nameLabel.text = label
|
||||
self.slider.value = Float(value)
|
||||
self.slider.minimumTrackTintColor = color
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
// MARK: - Layout
|
||||
func setupUI() {
|
||||
self.addSubview(self.nameLabel)
|
||||
self.nameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
|
||||
self.nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true
|
||||
self.nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true
|
||||
|
||||
self.addSubview(self.slider)
|
||||
self.slider.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
||||
self.slider.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 10).isActive = true
|
||||
let wc = self.slider.widthAnchor.constraint(equalToConstant: 216)
|
||||
wc.isActive = true
|
||||
self.sliderWidthConstraint = wc
|
||||
|
||||
self.slider.addTarget(self, action: #selector(self.sliderChanged), for: .valueChanged)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.sliderWidthConstraint?.constant = bounds.height * 0.6
|
||||
}
|
||||
|
||||
// MARK: - Update
|
||||
override func update(value: Int) {
|
||||
self.slider.value = Float(value)
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
@objc private func sliderChanged() {
|
||||
self.onSend?(self.uid, Int(self.slider.value))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import UIKit
|
||||
|
||||
class FeedbackWidget: BaseWidget {
|
||||
|
||||
static let defaultSize = CGSize(width: 220, height: 100)
|
||||
|
||||
static func defaultFrame(idx: Int) -> CGRect {
|
||||
let col = idx % 3
|
||||
let row = idx / 3
|
||||
return CGRect(x: 16 + CGFloat(col) * 250, y: 48 + CGFloat(row) * 120,
|
||||
width: defaultSize.width, height: defaultSize.height)
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
lazy var trackLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.textColor = UIColor(white: 0.9, alpha: 1)
|
||||
lbl.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
lbl.textAlignment = .left
|
||||
lbl.text = "—"
|
||||
return lbl
|
||||
}()
|
||||
|
||||
lazy var barLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.textColor = UIColor(white: 0.6, alpha: 1)
|
||||
lbl.font = .monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||
lbl.textAlignment = .left
|
||||
lbl.text = "Bar —"
|
||||
return lbl
|
||||
}()
|
||||
|
||||
lazy var stateLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.textColor = UIColor(white: 0.5, alpha: 1)
|
||||
lbl.font = .systemFont(ofSize: 20, weight: .medium)
|
||||
lbl.textAlignment = .right
|
||||
lbl.text = "■"
|
||||
return lbl
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
override init(uid: String, frame: CGRect) {
|
||||
super.init(uid: uid, frame: frame)
|
||||
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
// MARK: - Layout
|
||||
func setupUI() {
|
||||
self.addSubview(self.trackLabel)
|
||||
self.trackLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true
|
||||
self.trackLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12).isActive = true
|
||||
self.trackLabel.trailingAnchor.constraint(equalTo: self.stateLabel.leadingAnchor, constant: -8).isActive = true
|
||||
|
||||
self.addSubview(self.stateLabel)
|
||||
self.stateLabel.centerYAnchor.constraint(equalTo: self.trackLabel.centerYAnchor).isActive = true
|
||||
self.stateLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true
|
||||
|
||||
self.addSubview(self.barLabel)
|
||||
self.barLabel.topAnchor.constraint(equalTo: self.trackLabel.bottomAnchor, constant: 8).isActive = true
|
||||
self.barLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12).isActive = true
|
||||
self.barLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true
|
||||
}
|
||||
|
||||
// MARK: - Update
|
||||
func updateDaw(track: String?, bar: Int?, playing: Bool?) {
|
||||
if let track = track { self.trackLabel.text = track }
|
||||
if let bar = bar { self.barLabel.text = "Bar \(bar)" }
|
||||
if let playing = playing {
|
||||
self.stateLabel.text = playing ? "▶" : "■"
|
||||
self.stateLabel.textColor = playing ? UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1) : UIColor(white: 0.5, alpha: 1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
|
||||
class LogWidget: BaseWidget {
|
||||
|
||||
static let defaultSize = CGSize(width: 360, height: 660)
|
||||
|
||||
static func defaultFrame() -> CGRect {
|
||||
CGRect(x: 16, y: 80, width: defaultSize.width, height: defaultSize.height)
|
||||
}
|
||||
|
||||
private weak var hostedView: UIView?
|
||||
|
||||
override init(uid: String, frame: CGRect) {
|
||||
super.init(uid: uid, frame: frame)
|
||||
self.backgroundColor = UIColor(white: 0.08, alpha: 0.95)
|
||||
self.layer.borderColor = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 0.3).cgColor
|
||||
self.isHidden = true
|
||||
setupPinch()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func attach(to parentVC: UIViewController, loggingView: LoggingView) {
|
||||
parentVC.addChild(loggingView)
|
||||
loggingView.view.frame = self.bounds
|
||||
self.addSubview(loggingView.view)
|
||||
loggingView.didMove(toParent: parentVC)
|
||||
self.hostedView = loggingView.view
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
hostedView?.frame = self.bounds
|
||||
}
|
||||
|
||||
private func setupPinch() {
|
||||
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
|
||||
self.addGestureRecognizer(pinch)
|
||||
}
|
||||
|
||||
@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
|
||||
let scale = gesture.scale
|
||||
let newWidth = max(200, min(800, self.bounds.width * scale))
|
||||
let newHeight = max(100, min(600, self.bounds.height * scale))
|
||||
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: newWidth, height: newHeight))
|
||||
gesture.scale = 1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import UIKit
|
||||
|
||||
class TitleWidget: BaseWidget {
|
||||
|
||||
static func defaultFrame(idx: Int) -> CGRect {
|
||||
let col = idx % 3
|
||||
let row = idx / 3
|
||||
return CGRect(x: 16 + CGFloat(col) * 220, y: 16 + CGFloat(row) * 60, width: 200, height: 44)
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
lazy var titleLabel: UILabel = {
|
||||
let lbl = UILabel()
|
||||
lbl.translatesAutoresizingMaskIntoConstraints = false
|
||||
lbl.textColor = UIColor(white: 0.85, alpha: 1)
|
||||
lbl.font = .systemFont(ofSize: 22, weight: .semibold)
|
||||
lbl.textAlignment = .left
|
||||
return lbl
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
init(uid: String, text: String, frame: CGRect) {
|
||||
super.init(uid: uid, frame: frame)
|
||||
self.backgroundColor = .clear
|
||||
self.layer.borderColor = UIColor.clear.cgColor
|
||||
self.titleLabel.text = text
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
// MARK: - Layout
|
||||
func setupUI() {
|
||||
self.addSubview(self.titleLabel)
|
||||
self.titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true
|
||||
self.titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true
|
||||
self.titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import UIKit
|
||||
|
||||
class ToggleWidget: BaseWidget {
|
||||
|
||||
static let size = CGSize(width: 75, height: 75)
|
||||
|
||||
static func defaultFrame(idx: Int) -> CGRect {
|
||||
let gap: CGFloat = 12
|
||||
let col = idx % 6
|
||||
let row = idx / 6
|
||||
return CGRect(x: 16 + CGFloat(col) * (size.width + gap),
|
||||
y: 420 + CGFloat(row) * (size.height + gap),
|
||||
width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
let color: UIColor
|
||||
var isOn: Bool
|
||||
|
||||
// MARK: - Subviews
|
||||
lazy var button: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.layer.cornerRadius = 4
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
return btn
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
init(uid: String, label: String, isOn: Bool, color: UIColor, frame: CGRect) {
|
||||
self.color = color
|
||||
self.isOn = isOn
|
||||
super.init(uid: uid, frame: frame)
|
||||
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
self.button.setTitle(label, for: .normal)
|
||||
setupUI()
|
||||
applyState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
// MARK: - Layout
|
||||
func setupUI() {
|
||||
self.addSubview(self.button)
|
||||
self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
||||
self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
||||
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
||||
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
func applyState() {
|
||||
self.button.backgroundColor = self.isOn ? self.color : UIColor(white: 0.15, alpha: 1)
|
||||
self.button.setTitleColor(self.isOn ? .black : .lightGray, for: .normal)
|
||||
}
|
||||
|
||||
override func update(value: Int) {
|
||||
self.isOn = value > 63
|
||||
applyState()
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
@objc private func tapped() {
|
||||
self.isOn = !self.isOn
|
||||
applyState()
|
||||
self.onSend?(self.uid, self.isOn ? 127 : 0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import UIKit
|
||||
|
||||
class TransportWidget: BaseWidget {
|
||||
|
||||
static let size = CGSize(width: 75, height: 75)
|
||||
|
||||
static func defaultFrame(idx: Int) -> CGRect {
|
||||
let gap: CGFloat = 12
|
||||
let col = idx % 6
|
||||
let row = idx / 6
|
||||
return CGRect(x: 16 + CGFloat(col) * (size.width + gap),
|
||||
y: 420 + CGFloat(row) * (size.height + gap),
|
||||
width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
lazy var button: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.translatesAutoresizingMaskIntoConstraints = false
|
||||
btn.setTitleColor(.lightGray, for: .normal)
|
||||
btn.backgroundColor = UIColor(white: 0.15, alpha: 1)
|
||||
btn.layer.cornerRadius = 4
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
return btn
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
init(uid: String, label: String, frame: CGRect) {
|
||||
super.init(uid: uid, frame: frame)
|
||||
self.layer.borderColor = UIColor(white: 0.27, alpha: 1).cgColor
|
||||
self.button.setTitle(label, for: .normal)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
// MARK: - Layout
|
||||
func setupUI() {
|
||||
self.addSubview(self.button)
|
||||
self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
||||
self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
||||
self.button.widthAnchor.constraint(equalToConstant: 63).isActive = true
|
||||
self.button.heightAnchor.constraint(equalToConstant: 63).isActive = true
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
@objc private func tapped() {
|
||||
self.onSend?(self.uid, 127)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// SceneDelegate.swift
|
||||
// remote-client-ios
|
||||
//
|
||||
// Created by p4piwabl0 on 6/30/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = (scene as? UIWindowScene) else { return }
|
||||
window = UIWindow(windowScene: windowScene)
|
||||
window?.rootViewController = ViewController()
|
||||
window?.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Called when the scene has moved from an inactive state to an active state.
|
||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Called when the scene will move from an active state to an inactive state.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// remote_client_iosTests.swift
|
||||
// remote-client-iosTests
|
||||
//
|
||||
// Created by p4piwabl0 on 6/30/26.
|
||||
//
|
||||
|
||||
import Testing
|
||||
@testable import remote_client_ios
|
||||
|
||||
struct remote_client_iosTests {
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// remote_client_iosUITests.swift
|
||||
// remote-client-iosUITests
|
||||
//
|
||||
// Created by p4piwabl0 on 6/30/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class remote_client_iosUITests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLaunchPerformance() throws {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// remote_client_iosUITestsLaunchTests.swift
|
||||
// remote-client-iosUITests
|
||||
//
|
||||
// Created by p4piwabl0 on 6/30/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class remote_client_iosUITestsLaunchTests: XCTestCase {
|
||||
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLaunch() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user