I'm currently developing an app that has a single ViewController with a WKWebView object that acts as a client for a web app. It uses JavaScript communication and injection to allow for objects in native Swift to interact with the WebView.
My Objective
I have a function, urlDidChange(_ url: String), that fires whenever the WKWebView's raw URL value changes. I'm trying to dynamically set the orientation restrictions, depending on the value of said new url, and force-rotate the device to adapt those restrictions once a condition is met.
I'm not sure if this extra information is important, but thought that I'd include it anyway: The UIViewController is also embedded in a UINavigationController. The native NavBar really helps the client feel more like a native app. I haven't setup any custom classes for it and have simply been using let navBar = navigationController?.navigationBar.
Desired Example Usage
func urlDidChange(_ url: String) {
if url.contains("/dashboard") {
UIInterfaceOrientationMask = .portrait
} else if url.contains("/builder") {
UIInterfaceOrientationMask = [.portrait, .landscapeRight]
} else {
UIInterfaceOrientationMask = .all
}
// Set new orientation properties
// Force device rotation based on newly set properties
UIViewController.attemptRotationToDeviceOrientation()
}
Current Code
This is my current setup. I have attempted the following, with no luck:
enum Page {
var orientation: UIInterfaceOrientationMask {
switch self {
case .login: return getDeviceOrientation()
case .dashboard: return getDeviceOrientation()
case .newProject: return getDeviceOrientation()
case .builder: return getDeviceOrientation()
case .other: return getDeviceOrientation()
}
}
func getDeviceOrientation() -> UIInterfaceOrientationMask {
let phone = Device.isPhone()
switch self {
case .login:
if phone { return .portrait }
else { return .all }
case .dashboard:
if phone { return .portrait }
else { return .all }
case .newProject:
if phone { return .portrait }
else { return .all }
case .builder:
if phone { return [.portrait, .landscapeRight] }
else { return .all }
case .other:
if phone { return .portrait }
else { return .all }
}
}
ViewController
Finally, to apply the new orientation properties, I use this:
func urlDidChange(_ url: String) {
let page = Page.get(forURL: url) // Returns Page case for current URL
UIDevice.current.setValue(page.orientation.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
}
Bonus question
Is there a more efficient way of using enumerations to combine the Device properties with my Page enum (ie. case .builder with different return values for when Device.isPhone or when Device.isPad)?
As you can see in the code above, I'm simply using if statements to determine which output to provide right now:
case .builder:
if phone { return [.portrait, .landscapeRight] }
else { return .all }
If your ViewController is inside a UINavigationController, that might be the reason. Can you try using a custom UINavigationController, that retrieves the orientation from your ViewController? Something like this:
struct OrientationConfig {
let phone: UIInterfaceOrientationMask
let pad: UIInterfaceOrientationMask
static let defaultConfig = OrientationConfig(phone: .portrait, pad: .all)
}
enum Page {
case login
case dashboard
case newProject
case builder
case other
static let orientationConfigs: [Page:OrientationConfig] = [
.login: OrientationConfig.defaultConfig,
.dashboard: OrientationConfig.defaultConfig,
.newProject: OrientationConfig.defaultConfig,
.builder: OrientationConfig(phone: [.portrait, .landscapeRight],
pad: .all),
.other: OrientationConfig.defaultConfig
]
}
class MyViewController: UIViewController {
var page: Page = .login
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if let config = Page.orientationConfigs[page] {
return Device.isPhone ? config.phone : config.pad
}
return super.supportedInterfaceOrientations
}
}
class MyNavigationController: UINavigationController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if let viewController = topViewController as? MyViewController {
return viewController.supportedInterfaceOrientations
}
return super.supportedInterfaceOrientations
}
}
Related
I'm using a TabBarcontroller type app and I'm using a shared model of the form:
enum WorkoutState {
case Stopped
case Started
case Paused
}
class BaseTBController: UITabBarController {
var workoutState: WorkoutState? = .Stopped
}
Currently all is working and I can access and update the variable across the different tabs using
let tabbar = tabBarController as! BaseTBController
if tabbar.workoutState = .Stop {
//do something
tabbar.workoutState = .Start
}
Now, the situation is that I seem to need to put this all over the place in my code. eg:
startRun()
resumeRun()
pauseRun()
Is there a better way to do this instead of putting
let tabbar = tabBarController as! BaseTBController
tabbar.workoutState = .Start
in each of the 3 functions?
You can always use protocol and default extension to achieve what you need
protocol HandleWorkStateProtocol where Self: UIViewController {
func updateWorkOutState(to: WorkoutState)
}
extension HandleWorkStateProtocol {
func updateWorkOutState(to state: WorkoutState) {
guard let tabBarController = self.tabBarController as? BaseTBController else { return }
tabBarController.workoutState = state
}
}
In all you view controller's that has these 3 methods (startRun, resumeRun, pauseRun) simply confirm to this protocol and call updateWorkOutState(to: with appropriate value to modify the status
class SomeTestViewController: UIViewController {
func startRun() {
self.updateWorkOutState(to: .Started)
}
func resumeRun() {
}
func pauseRun() {
self.updateWorkOutState(to: .Paused)
}
}
extension SomeTestViewController: HandleWorkStateProtocol {}
P.S
Case values of enum does not follow Pascal casing like Stopped instead it follows Camel casing stopped so change your enum values to
enum WorkoutState {
case stopped
case started
case paused
}
Say I have the following:
class ContentSelectableViewController<T: NSManagedObject> : UIViewController { //... }
class PersonSelectionViewController: ContentSelectableViewController<Person> { // ... }
class PlaceSelectionViewController: ContentSelectableViewController<Place> { // ... }
Then in an instance of one of these subclasses, I have some code:
if let navCtrl = self.navigationController {
for viewController in navCtrl.viewControllers.reversed() {
if viewController is ContentSelectableViewController {
log.info("Worked for \(viewController.description)")
}
if let vc = viewController as? ContentSelectableViewController {
// This should be equivalent to the above.
}
}
}
My question is, when I have a stack full of subclasses of this generic baseclass, it doesn't always return true (go into the if statement) when checking if they are of type ContentSelectableViewController and I don't understand why. They inherit from the same baseclass.
EDIT:
I'm guessing it's because of the generic nature of the class. The if statements evaluate to true for the subclass that calls it.
So, it does in fact have something to do with trying to type check a generic class. It would work for the one and not the other because the one making the call implicitly adds its type.
i.e. (Pseudo-Swift)
if viewController is ContentSelectableViewController<Person> { //... }
What I did instead was to define a protocol that ultimately makes these ContentSelectableViewController<T> selectable:
enum ContentSelectionRole: Int {
case none = 0 // no selection going on right now.
case root // i.e. the one wanting content
case branch // an intermediary. think of a folder when looking for a file
case leaf // like a file
}
enum ContentSelectability: Int {
case noSelections = 0
case oneSelection = 1
case multipleSelections = 2
}
protocol ContentSelection {
var selectedObjects: [NSManagedObject] { get set }
var selectionRole: ContentSelectionRole { get set }
var selectionStyle: ContentSelectability { get set }
func popToSelectionRootViewController() -> Bool
func willNavigateBack(from viewController: UIViewController)
}
Making the definition:
class ContentSelectableViewController<T: NSManagedObject> : UIViewController, ContentSelection { //... }
And then, refactored the original post, to get:
#discardableResult func popToSelectionRootViewController() -> Bool {
if let navCtrl = self.navigationController {
for viewController in navCtrl.viewControllers.reversed() {
if let vc = viewController as? ContentSelection {
if vc.selectionRole == .root {
vc.willNavigateBack(from: self)
navCtrl.popToViewController(viewController, animated: true)
return true
}
}
}
}
return false
}
I still don't quite understand the aspect of the language that makes it fail, but this solution works.
Protocol-based Programming seems to be more Swifty anyway...
Does anyone have an example of a simple state machine that can be implemented in Swift? I've been googling around, but have only seen a lot of third party libraries which are pretty opaque to me. I've also seen a lot of high level discussions where people talk about state machines, but haven't shown any code. Is it possible for someone to do a simple counter example (like add 1 / subtract 1)? Or point me to one? I know it's a big ask, but my google-fu is failing me. Thank you.
Apple's GamePlayKit already has state machine implementation as GKStateMachine. But GamePlayKit is available only from iOS 9 so I had to clone GKStateMachine (and GKState) to support iOS 8 in one of my apps. Hope it will be helpful.
EHStateMachine.swift
import Foundation
class EHStateMachine {
private(set) var currentState: EHState?
private let states = NSMapTable<AnyObject, EHState>(keyOptions: [.objectPointerPersonality],
valueOptions: [.strongMemory])
// MARK:
init(states: [EHState]) {
guard states.count > 0 else {
fatalError("Can't create state machine with zero states.")
}
let tempStates = NSHashTable<AnyObject>(options: [.objectPointerPersonality])
for state in states {
guard !tempStates.contains(type(of: state)) else {
fatalError("Duplicate instances of \(type(of: state)) found.")
}
tempStates.add(type(of: state))
}
for state in states {
state.stateMachine = self
self.states.setObject(state, forKey: type(of: state))
}
}
// MARK:
func canEnterState(_ stateClass: AnyClass) -> Bool {
if (states.object(forKey: stateClass) == nil) {
return false
}
if currentState == nil {
return true
}
return currentState!.isValidNextState(stateClass)
}
func enter(_ stateClass: AnyClass) -> Bool {
if !canEnterState(stateClass) {
return false
}
let previousState = currentState
let nextState = states.object(forKey: stateClass)
previousState?.willExit(to: nextState!)
currentState = nextState
currentState!.didEnter(from: previousState)
return true
}
func update(deltaTime seconds: TimeInterval) {
currentState?.update(deltaTime: seconds)
}
func state<T: EHState>(forClass stateClass: T.Type) -> T? {
return states.object(forKey: stateClass) as? T
}
}
EHState.swift
import Foundation
class EHState {
weak var stateMachine: EHStateMachine?
// MARK:
func isValidNextState(_ stateClass: AnyClass) -> Bool {
return true
}
func didEnter(from previousState: EHState?) {
}
func willExit(to nextState: EHState) {
}
func update(deltaTime seconds: TimeInterval) {
}
}
If you target iOS 9 you should probably use GKStateMachine and GKState instead of these.
In my swift app I'm allowing users to take photos.
For that purpose I've decided to use CameraManager from here: https://github.com/imaginary-cloud/CameraManager
When user opens my app, he sees a button - when he presses it, the camera view appears and he can take a photo. He also can dismiss the camera view and later on, at some point, press the button one more time to open camera view again.
If I understand it correctly from the plugin docs, I need to add a camera view to my view during first usage, then - in case of dismiss - invoke stopCaptureSession(), and during every next usage call resumeCaptureSession().
Currently in my swift code I have three methods:
let cameraManager = CameraManager()
fileprivate func addCameraToView()
{
cameraManager.addPreviewLayerToView(cameraView, newCameraOutputMode: CameraOutputMode.stillImage)
}
fileprivate func stopCaptureSession() {
cameraManager.stopCaptureSession()
}
fileprivate func resumeCaptreSession() {
cameraManager.resumeCaptureSession()
}
The IBAction for the button has the following code:
let currentCameraState = cameraManager.currentCameraStatus()
if currentCameraState == .notDetermined {
cameraManager.askUserForCameraPermission({ permissionGranted in
if permissionGranted {
self.resumeCaptreSession()
}
})
} else if (currentCameraState == .ready) {
self.resumeCaptreSession()
} else {
print("we do not have access to camera")
}
and in the IBAction for the dismiss button I had:
print("cancelling camera")
stopCaptureSession()
To make it work properly, I need to call addCameraToView() somewhere earlier - until now I was adding it in viewDidLoad, but I realized that I cannot do that because while doing so - the camera stays active until user presses the dismiss button.
So I thought about changing my code in IBAction for the camera button and add a camera from there. However, I have to add it only in case it wasn't add before - in the other case I need to call resumeCaptureSession().
The problem is that in CameraManager the function responsible for adding camera to the view is declared like this:
open func addPreviewLayerToView(_ view: UIView, newCameraOutputMode: CameraOutputMode) -> CameraState {
return addLayerPreviewToView(view, newCameraOutputMode: newCameraOutputMode, completion: nil)
}
open func addLayerPreviewToView(_ view: UIView, newCameraOutputMode: CameraOutputMode, completion: ((Void) -> Void)?) -> CameraState {
if _canLoadCamera() {
if let _ = embeddingView {
if let validPreviewLayer = previewLayer {
validPreviewLayer.removeFromSuperlayer()
}
}
if cameraIsSetup {
_addPreviewLayerToView(view)
cameraOutputMode = newCameraOutputMode
if let validCompletion = completion {
validCompletion()
}
} else {
_setupCamera({ Void -> Void in
self._addPreviewLayerToView(view)
self.cameraOutputMode = newCameraOutputMode
if let validCompletion = completion {
validCompletion()
}
})
}
}
return _checkIfCameraIsAvailable()
}
and resumeCaptureSession() is defined like this:
open func resumeCaptureSession() {
if let validCaptureSession = captureSession {
if !validCaptureSession.isRunning && cameraIsSetup {
validCaptureSession.startRunning()
_startFollowingDeviceOrientation()
}
} else {
if _canLoadCamera() {
if cameraIsSetup {
stopAndRemoveCaptureSession()
}
_setupCamera({Void -> Void in
if let validEmbeddingView = self.embeddingView {
self._addPreviewLayerToView(validEmbeddingView)
}
self._startFollowingDeviceOrientation()
})
}
}
}
So my question is - when user opens camera view, how can I check if camera was added to the view before, and if it was added - call resumeCaptureSession(), otherwise do not call it and just leave it with calling addCameraToView?
Recently I watched this talk (link, see section Networking) and I decided to do some excerise in protocol-oriented programming. So I thought about this simple example: View Controller for displaying list of files. Of course, protocol-oriented way, with following constraints:
FilesViewController - containts table view & FilesTableViewAdapter. Table view delegate.
FilesTableViewAdapter - initializable with table view & FilesProvider: Gettable,
so that in tests I can inject FilesProviderMock: Gettable.
FilesTableViewAdapter is a data source of table view and uses FilesProvider for fetching files.
final class FilesTableViewController: UIViewController {
var filesTableView: FilesTableView! { return view as! FilesTableView }
private var tableViewAdapter: FilesTableViewAdapter<FilesProvider>!
// MARK: Subclassing
override func loadView() {
view = FilesTableView(frame: UIScreen.main.bounds)
}
override func viewDidLoad() {
tableViewAdapter = FilesTableViewAdapter(filesTableView.tableView, provider: FilesProvider())
// Actually I would like to have this method in Adapter
// so that VC isn't handling networking.
tableViewAdapter.provider.get { result in
// result type: (Result<[File]>)
switch result {
case .success(let files): print(files)
case .failure(let error): print(error)
}
}
filesTableView.tableView.delegate = self
filesTableView.tableView.dataSource = tableViewAdapter
}
}
extension FilesTableViewController: UITableViewDelegate {
//
}
final class FilesTableViewAdapter<T: Gettable>: NSObject, UITableViewDataSource {
let provider: T
private let tableView: UITableView
init(_ tableView: UITableView, provider: T) {
self.tableView = tableView
self.provider = provider
super.init()
}
func problem() {
provider.get { result in
// Result type is (Result<T.T>) - :(
switch result {
case .success(let files): print(files)
case .failure(let error): print(error)
}
}
}
struct FilesProvider {
private let Files = [File]()
}
extension FilesProvider: Gettable {
func get(completionHandler: (Result<[File]>) -> Void) {
//
}
}
protocol Gettable {
associatedtype T
func get(completionHandler: (Result<T>) -> Void)
}
I know I went too far with generalizing this piece of code. Now I'm stuck and I have this questions I can't answer myself:
How to make it in protocol-oriented way, with networking code in class different than VC (say Adapter)?
How to make it easily testable and extendable in the future?