Skip to content

Commit

Permalink
Swift-y ViewOption enum & SCNView/Controller convenience inits
Browse files Browse the repository at this point in the history
`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.
  • Loading branch information
capnslipp committed Dec 29, 2022
1 parent 4743c22 commit 3179c70
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 10 deletions.
8 changes: 8 additions & 0 deletions SCNViewController.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -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 = "<group>"; };
FAA5DBA1295E29240004886A /* ViewOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewOption.swift; sourceTree = "<group>"; };
FAA5DBA2295E29240004886A /* SCNViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SCNViewExtensions.swift; sourceTree = "<group>"; };
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 = "<group>"; };
OBJ_9 /* SCNViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCNViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -96,6 +100,8 @@
isa = PBXGroup;
children = (
OBJ_9 /* SCNViewController.swift */,
FAA5DBA2295E29240004886A /* SCNViewExtensions.swift */,
FAA5DBA1295E29240004886A /* ViewOption.swift */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down
23 changes: 15 additions & 8 deletions Sources/SCNViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
15 changes: 15 additions & 0 deletions Sources/SCNViewExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SCNViewExtensions
// @author: Slipp Douglas Thompson
// @license: Public Domain per The Unlicense. See accompanying LICENSE file or <http://unlicense.org/>.

import SceneKit



public extension SCNView
{
convenience init(frame: CGRect, options: [SCNViewController.ViewOption]? = nil)
{
self.init(frame: frame, options: options?.asObjCStyleOptions)
}
}
71 changes: 71 additions & 0 deletions Sources/ViewOption.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// ViewOptions
// @author: Slipp Douglas Thompson
// @license: Public Domain per The Unlicense. See accompanying LICENSE file or <http://unlicense.org/>.

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
}
}
5 changes: 3 additions & 2 deletions Tests/SCNViewControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]
)

Expand All @@ -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)

}
}
Expand Down

0 comments on commit 3179c70

Please sign in to comment.