From 3179c70d0e7078babe3ad1d16c84b0b045d4f098 Mon Sep 17 00:00:00 2001 From: Slipp Douglas Thompson Date: Mon, 14 Nov 2022 22:27:15 -0600 Subject: [PATCH] Swift-y `ViewOption` enum & SCNView/Controller convenience inits `SCNView` still hasn't gotten a modern Swift associated-type-enum type for its options init param (still using unchecked Obj-C-ish [String:Any]), so I'm just adding my own `ViewOption` enum with conversions to/from ObjC-ish dictionaries. * Created `SCNViewController.ViewOption` enum with associated types for each case. * Gave `SCNViewController` a convenience init that takes a `[ViewOption]`. * Gave `SCNView` a convenience init that takes a `[ViewOption]`. * Updated tests to use `[ViewOption]` init variant. --- SCNViewController.xcodeproj/project.pbxproj | 8 +++ Sources/SCNViewController.swift | 23 ++++--- Sources/SCNViewExtensions.swift | 15 +++++ Sources/ViewOption.swift | 71 +++++++++++++++++++++ Tests/SCNViewControllerTests.swift | 5 +- 5 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 Sources/SCNViewExtensions.swift create mode 100644 Sources/ViewOption.swift diff --git a/SCNViewController.xcodeproj/project.pbxproj b/SCNViewController.xcodeproj/project.pbxproj index ee3f5eb..67a1595 100644 --- a/SCNViewController.xcodeproj/project.pbxproj +++ b/SCNViewController.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ FA6A867722765A5200EA62B4 /* SceneKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA6A867322765A3600EA62B4 /* SceneKit.framework */; }; FAA5DB9A295E17D60004886A /* SCNViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA5DB99295E17D60004886A /* SCNViewControllerTests.swift */; }; FAA5DB9B295E17D60004886A /* SCNViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* SCNViewController.framework */; platformFilters = (ios, tvos, ); }; + FAA5DBA3295E29240004886A /* ViewOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA5DBA1295E29240004886A /* ViewOption.swift */; }; + FAA5DBA4295E29240004886A /* SCNViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA5DBA2295E29240004886A /* SCNViewExtensions.swift */; }; OBJ_18 /* SCNViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* SCNViewController.swift */; }; /* End PBXBuildFile section */ @@ -29,6 +31,8 @@ FA6A867522765A4B00EA62B4 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; FAA5DB97295E17D60004886A /* SCNViewControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SCNViewControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FAA5DB99295E17D60004886A /* SCNViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCNViewControllerTests.swift; sourceTree = ""; }; + FAA5DBA1295E29240004886A /* ViewOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewOption.swift; sourceTree = ""; }; + FAA5DBA2295E29240004886A /* SCNViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SCNViewExtensions.swift; sourceTree = ""; }; OBJ_12 /* SCNViewController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SCNViewController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; OBJ_9 /* SCNViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCNViewController.swift; sourceTree = ""; }; @@ -96,6 +100,8 @@ isa = PBXGroup; children = ( OBJ_9 /* SCNViewController.swift */, + FAA5DBA2295E29240004886A /* SCNViewExtensions.swift */, + FAA5DBA1295E29240004886A /* ViewOption.swift */, ); path = Sources; sourceTree = ""; @@ -196,7 +202,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + FAA5DBA4295E29240004886A /* SCNViewExtensions.swift in Sources */, OBJ_18 /* SCNViewController.swift in Sources */, + FAA5DBA3295E29240004886A /* ViewOption.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/SCNViewController.swift b/Sources/SCNViewController.swift index 3ae10c0..53c1f40 100644 --- a/Sources/SCNViewController.swift +++ b/Sources/SCNViewController.swift @@ -15,42 +15,49 @@ public class SCNViewController : UIViewController } - /// Unfortunately, SCNView's API hasn't yet been fully updated for Swift, so if you use `viewOptions`s they need to be specified similar to the following: + /// The `viewFrame` and `viewOptions` arguments are ignored if a `nibName` is specified (values come from the nib's SCNView object). + public convenience init(nibName:String?, bundle nibBundle:Bundle?=nil, viewFrame:CGRect?, viewOptions:[ViewOption]?=nil) { + self.init(nibName: nibName, bundle: nibBundle, viewFrame: viewFrame, viewOptions: viewOptions?.asObjCStyleOptions) + } + + /// Uses SCNView-API-like viewOptions (Obj-C-style), like the following: /// viewOptions: [ /// SCNView.Option.preferredRenderingAPI.rawValue: NSNumber(value: SCNRenderingAPI.metal.rawValue), /// SCNView.Option.preferredDevice.rawValue: MTLCreateSystemDefaultDevice()!, /// SCNView.Option.preferLowPowerDevice.rawValue: NSNumber(value: true) /// ] - public required init(nibName:String?, bundle nibBundle:Bundle?=nil, viewFrame:CGRect?, viewOptions:[String:Any]?=[:]) + /// The `viewFrame` and `viewOptions` arguments are ignored if a `nibName` is specified (values come from the nib). + public required init(nibName:String?, bundle nibBundle:Bundle?=nil, viewFrame:CGRect?, viewOptions:[SCNView.Option.RawValue:Any]?=nil) { if nibName == nil { _initViewFrame = viewFrame ?? CGRect.null _initViewOptions = viewOptions } else { _initViewFrame = CGRect.null - _initViewOptions = nil + _initViewOptions = [:] } super.init(nibName: nibName, bundle: nibBundle) } - public convenience init(viewFrame:CGRect?, viewOptions:[String:Any]? = [:]) { + + public convenience init(viewFrame:CGRect?, viewOptions:[ViewOption] = []) { self.init(nibName: nil, bundle: nil, viewFrame: viewFrame, viewOptions: viewOptions) } public convenience override init(nibName:String?, bundle nibBundle:Bundle?=nil) { - self.init(nibName: nibName, bundle: nibBundle, viewFrame: nil, viewOptions: nil) + self.init(nibName: nibName, bundle: nibBundle, viewFrame: nil, viewOptions: []) } public required init?(coder aDecoder:NSCoder) { _initViewFrame = CGRect.null - _initViewOptions = nil + _initViewOptions = [:] super.init(coder: aDecoder) } private let _initViewFrame:CGRect - private let _initViewOptions:[String:Any]? + private let _initViewOptions:[SCNView.Option.RawValue:Any]? @objc public var scnView:SCNView { @@ -70,7 +77,7 @@ public class SCNViewController : UIViewController } self.view = { - let view = SCNView(frame: _initViewFrame, options: _initViewOptions) + let view = SCNView(frame: _initViewFrame, options: _initViewOptions) if #available(iOS 9.0, tvOS 9.0, *), NSClassFromString("AVAudioEngine") != nil { _ = view.audioEngine } diff --git a/Sources/SCNViewExtensions.swift b/Sources/SCNViewExtensions.swift new file mode 100644 index 0000000..7400f37 --- /dev/null +++ b/Sources/SCNViewExtensions.swift @@ -0,0 +1,15 @@ +// SCNViewExtensions +// @author: Slipp Douglas Thompson +// @license: Public Domain per The Unlicense. See accompanying LICENSE file or . + +import SceneKit + + + +public extension SCNView +{ + convenience init(frame: CGRect, options: [SCNViewController.ViewOption]? = nil) + { + self.init(frame: frame, options: options?.asObjCStyleOptions) + } +} diff --git a/Sources/ViewOption.swift b/Sources/ViewOption.swift new file mode 100644 index 0000000..045e460 --- /dev/null +++ b/Sources/ViewOption.swift @@ -0,0 +1,71 @@ +// ViewOptions +// @author: Slipp Douglas Thompson +// @license: Public Domain per The Unlicense. See accompanying LICENSE file or . + +import SceneKit + + + +public extension SCNViewController +{ + enum ViewOption : Equatable + { + case preferLowPowerDevice(Bool) + case preferredDevice(MTLDevice) + case preferredRenderingAPI(SCNRenderingAPI) + + + public static func == (lhs: ViewOption, rhs: ViewOption) -> Bool { + switch (lhs, rhs) { + case (.preferLowPowerDevice(let lhsValue), .preferLowPowerDevice(let rhsValue)): + return lhsValue == rhsValue + case (.preferredDevice(let lhsValue), .preferredDevice(let rhsValue)): + if #available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, *) { + return lhsValue.registryID == rhsValue.registryID + } else { + return lhsValue.name == rhsValue.name + } + case (.preferredRenderingAPI(let lhsValue), .preferredRenderingAPI(let rhsValue)): + return lhsValue == rhsValue + default: + return false + } + } + } +} + + + +extension Array where Element == SCNViewController.ViewOption +{ + public init(objcStyleOptions: [SCNView.Option.RawValue:Any]) + { + self = [] + if let objcValue = objcStyleOptions[SCNView.Option.preferLowPowerDevice.rawValue] { + append(.preferLowPowerDevice((objcValue as! NSNumber).boolValue)) + } + if let objcValue = objcStyleOptions[SCNView.Option.preferredDevice.rawValue] { + append(.preferredDevice(objcValue as! MTLDevice)) + } + if let objcValue = objcStyleOptions[SCNView.Option.preferredRenderingAPI.rawValue] { + append(.preferredRenderingAPI(SCNRenderingAPI(rawValue: (objcValue as! NSNumber).uintValue)!)) + } + } + + public var asObjCStyleOptions: [SCNView.Option.RawValue:Any] { + var objcStyleOptions: [SCNView.Option.RawValue:Any] = [:] + + for element in self { + switch element { + case .preferLowPowerDevice(let value): + objcStyleOptions[SCNView.Option.preferLowPowerDevice.rawValue] = NSNumber(value: value) + case .preferredDevice(let value): + objcStyleOptions[SCNView.Option.preferredDevice.rawValue] = value + case .preferredRenderingAPI(let value): + objcStyleOptions[SCNView.Option.preferredRenderingAPI.rawValue] = NSNumber(value: value.rawValue) + } + } + + return objcStyleOptions + } +} diff --git a/Tests/SCNViewControllerTests.swift b/Tests/SCNViewControllerTests.swift index c8682a6..b3085ce 100644 --- a/Tests/SCNViewControllerTests.swift +++ b/Tests/SCNViewControllerTests.swift @@ -28,7 +28,8 @@ final class SCNViewControllerTests: XCTestCase nibName: nil, viewFrame: CGRect(x: 0, y: 0, width: 1024, height: 768), viewOptions: [ - SCNView.Option.preferredRenderingAPI.rawValue : NSNumber(value: SCNRenderingAPI.openGLES2.rawValue), + .preferredRenderingAPI(.openGLES2), + .preferLowPowerDevice(true) ] ) @@ -39,7 +40,7 @@ final class SCNViewControllerTests: XCTestCase XCTAssertTrue(view is SCNView) if let scnView = view as? SCNView { - XCTAssertEqual(scnView.renderingAPI, SCNRenderingAPI.openGLES2) + XCTAssertEqual(scnView.renderingAPI, .openGLES2) } }