my app uses several libraryies. Bassically KYDrawerController. I want my App to use only potraite Orientation but for one ViewControllar i need both landscape and Potrait.
I have search about allmost every solution on the internet. every solution was to subclass the NavigationControllar and Override ShouldAutoRotate method. but non of them worked for me.
and here is the closer View of the entire StoryBoard
the gray color View is the KYDrawerControllar view which is an libry uses to work as NavigationDrawer like Android.
I have created a custom Class for Navigation Controllar and subclass it to the requierd ViewControllar's Navigation Controllar.
here is the Code
class SettingsNavigation: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
NSLog("visibleControllar", self.visibleViewController!.debugDescription)
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func shouldAutorotate() -> Bool {
//return self.visibleViewController!.shouldAutorotate()
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.Portrait
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
here is the ViewControllar
class Settings: UIViewController {
// MARK: - Outlets
#IBOutlet var profileImage: UIImageView!
#IBOutlet var profileName: UILabel!
#IBOutlet var profileRegistrationType: UILabel!
#IBOutlet var logOutButton: FUIButton!
#IBOutlet var subscriptionType: UILabel!
#IBOutlet var subscriptionRegistrationType: UILabel!
#IBOutlet var upgradeSubscriptionButton: FUIButton!
override func shouldAutorotate() -> Bool {
/* if (UIDevice.currentDevice().orientation == UIDeviceOrientation.LandscapeLeft ||
UIDevice.currentDevice().orientation == UIDeviceOrientation.LandscapeRight ||
UIDevice.currentDevice().orientation == UIDeviceOrientation.Unknown) {
return false;
}
else {
return true;
}*/
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
} }
and i am using StoryBoard segues to present ViewControllars.
please someone help me with this.
Here is my way:
in your appdelegae.swift:
var shouldSupportAllOrientation = false
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if (shouldSupportAllOrientation == true){
return UIInterfaceOrientationMask.All
}
return UIInterfaceOrientationMask.Portrait
}
in your entry view which enter the all orientation view (change to support all orientation, here I use a button as an example):
#IBAction func next(sender: UIButton) {
let appdelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appdelegate.shouldSupportAllOrientation = true
self.performSegueWithIdentifier("next", sender: self)
}
in your entry view which enter the all orientation view (change the orientation to support only Portrait):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let appdelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appdelegate.shouldSupportAllOrientation = false
}
finally, you may find this works on all iPhone device and iPad ipad2 except iPad air iPad pro; you should check the "requires full screen" in your project general info to ensure you all orientation view can enter landscape.
Related
I am using the jonkykong SideMenu in a Swift 5 xcode project. The following code worked fine before 6.4.8.
import UIKit
import SideMenu
class SideMenuViewController: UITableViewController {
#IBOutlet weak var headerView: UIView!
#IBOutlet weak var driverNameLabel: UILabel!
#IBOutlet weak var vehicleRegoLabel: UILabel!
var menuItems = [Dictionary<String, String>]()
override func viewDidLoad() {
super.viewDidLoad()
// Config options for the Slide in side menu
SideMenuManager.default.leftMenuNavigationController?.presentationStyle = .menuSlideIn
SideMenuManager.default.leftMenuNavigationController?.presentationStyle.onTopShadowOpacity = 1
// hides the 1px bottom border on the nav bar so the header seemless merges to the navbar
self.navigationController?.navigationBar.setValue(true, forKey: "hidesShadow")
... rest of controller code
After upgrading to 6.4.8 the presentationStyle has no effect. The presentationStyle is always the default of viewSlideOut regardless of what I set in the above code.
I am unable to find anything in the README about changes to this in the latest version. Any assistance would be greatly appreciated.
Please set presentationStyle in SideMenuSettings and assign it to SideMenuNavigationController.
func makeSettings() -> SideMenuSettings{
var settings = SideMenuSettings()
settings.allowPushOfSameClassTwice = false
settings.presentationStyle = .menuSlideIn
settings.statusBarEndAlpha = 0
return settings
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let sideMenuNavigationController = segue.destination as? SideMenuNavigationController else { return }
sideMenuNavigationController.settings = makeSettings()
}
TL;DR
Can't seem to use binding to tell wrapped AVPlayer to stop — why not? The "one weird trick" from Vlad works for me, without state & binding, but why?
See Also
My question is something like this one but that poster wanted to wrap an AVPlayerViewController and I want to control playback programmatically.
This guy also wondered when updateUIView() was called.
What happens (Console logs shown below.)
With code as shown here,
The user taps "Go to Movie"
MovieView appears and the vid plays
This is because updateUIView(_:context:) is being called
The user taps "Go back Home"
HomeView reappears
Playback halts
Again updateUIView is being called.
See Console Log 1
But... remove the ### line, and
Playback continues even when the home view returns
updateUIView is called on arrival but not departure
See Console log 2
If you uncomment the %%% code (and comment out what precedes it)
You get code I thought was logically and idiomatically correct SwiftUI...
...but "it doesn't work". I.e. the vid plays on arrival but continues on departure.
See Console log 3
The code
I do use an #EnvironmentObject so there is some sharing of state going on.
Main content view (nothing controversial here):
struct HomeView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
ZStack() { // +++ Weird trick ### fails if this is Group(). Wtf?
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}
which uses one of these (still routine declarative SwiftUI):
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// #State private var isPlaying: Bool = false // %%%
var body: some View {
VStack() {
PlayerView()
// PlayerView(isPlaying: $isPlaying) // %%%
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
print("> onAppear()")
self.router.isPlayingAV = true
// self.isPlaying = true // %%%
print("< onAppear()")
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
// self.isPlaying = false // %%%
print("< onDisappear()")
}
}
}
Now we get into the AVKit-specific stuff. I use the approach described by Chris Mash.
The aforementioned PlayerView, the wrappER:
struct PlayerView: UIViewRepresentable {
#EnvironmentObject var router: ViewRouter
// #Binding var isPlaying: Bool // %%%
private var myUrl : URL? { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }
func makeUIView(context: Context) -> PlayerView {
PlayerUIView(frame: .zero , url : myUrl)
}
// ### This one weird trick makes OS call updateUIView when view is disappearing.
class DummyClass { } ; let x = DummyClass()
func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {
print("> updateUIView()")
print(" router.isPlayingAV = \(router.isPlayingAV)")
// print(" isPlaying = \(isPlaying)") // %%%
// This does work. But *only* with the Dummy code ### included.
// See also +++ comment in HomeView
if router.isPlayingAV { v.player?.pause() }
else { v.player?.play() }
// This logic looks reversed, but is correct.
// If it's the other way around, vid never plays. Try it!
// if isPlaying { v?.player?.play() } // %%%
// else { v?.player?.pause() } // %%%
print("< updateUIView()")
}
}
And the wrappED UIView:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, url: URL?) {
super.init(frame: frame)
guard let u = url else { return }
self.player = AVPlayer(url: u)
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
And of course the view router, based on the Blckbirds example
class ViewRouter : ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter, Never>()
enum Page { case home, movie }
var page = Page.home { didSet { objectWillChange.send(self) } }
// Claim: App will never play more than one vid at a time.
var isPlayingAV = false // No didSet necessary.
}
Console Logs
Console log 1 (playing stops as desired)
> updateUIView() // First call
router.isPlayingAV = false // Vid is not playing => play it.
< updateUIView()
> onAppear()
< onAppear()
> updateUIView() // Second call
router.isPlayingAV = true // Vid is playing => pause it.
< updateUIView()
> onDisappear() // After the fact, we clear
< onDisappear() // the isPlayingAV flag.
Console log 2 (weird trick disabled; playing continues)
> updateUIView() // First call
router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
// No second call.
> onDisappear()
< onDisappear()
Console log 3 (attempt to use state & binding; playing continues)
> updateUIView()
isPlaying = false
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()
isPlaying = true
< updateUIView()
> updateUIView()
isPlaying = true
< updateUIView()
> onDisappear()
< onDisappear()
Well... on
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
print("< onDisappear()")
}
this is called after view is removed (it is like didRemoveFromSuperview, not will...), so I don't see anything bad/wrong/unexpected in that subviews (or even it itself) is not updated (in this case updateUIView)... I would rather surprise if it would be so (why update view, which is not in view hierarchy?!).
So this
class DummyClass { } ; let x = DummyClass()
is rather some wild bug, or ... bug. Forget about it and never use such stuff in releasing products.
OK, one would now ask, how to do with this? The main issue I see here is design-originated, specifically tight-coupling of model and view in PlayerUIView and, as a result, impossibility to manage workflow. AVPlayer here is not part of view - it is model and depending on its states AVPlayerLayer draws content. Thus the solution is to tear apart those entities and manage separately: views by views, models by models.
Here is a demo of modified & simplified approach, which behaves as expected (w/o weird stuff and w/o Group/ZStack limitations), and it can be easily extended or improved (in model/viewmodel layer)
Tested with Xcode 11.2 / iOS 13.2
Complete module code (can be copy-pasted in ContentView.swift in project from template)
import SwiftUI
import Combine
import AVKit
struct MovieView: View {
#EnvironmentObject var router: ViewRouter
// just for demo, but can be interchangable/modifiable
let playerModel = PlayerViewModel(url: Bundle.main.url(forResource: "myVid", withExtension: "mp4")!)
var body: some View {
VStack() {
PlayerView(viewModel: playerModel)
Button(action: { self.router.page = .home }) {
Text("Go back Home")
}
}.onAppear {
self.playerModel.player?.play() // << changes state of player, ie model
}.onDisappear {
self.playerModel.player?.pause() // << changes state of player, ie model
}
}
}
class PlayerViewModel: ObservableObject {
#Published var player: AVPlayer? // can be changable depending on modified URL, etc.
init(url: URL) {
self.player = AVPlayer(url: url)
}
}
struct PlayerView: UIViewRepresentable { // just thing wrapper, as intended
var viewModel: PlayerViewModel
func makeUIView(context: Context) -> PlayerUIView {
PlayerUIView(frame: .zero , player: viewModel.player) // if needed viewModel can be passed completely
}
func updateUIView(_ v: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
}
}
class ViewRouter : ObservableObject {
enum Page { case home, movie }
#Published var page = Page.home // used native publisher
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
var player: AVPlayer?
init(frame: CGRect, player: AVPlayer?) { // player is a model so inject it here
super.init(frame: frame)
self.player = player
self.playerLayer.player = player
self.layer.addSublayer(playerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
required init?(coder: NSCoder) { fatalError("not implemented") }
}
struct ContentView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
Group {
if router.page == .home {
Button(action: { self.router.page = .movie }) {
Text("Go to Movie")
}
} else if router.page == .movie {
MovieView()
}
}
}
}
Coordinator pattern is an old topic with many libraries trying to solve it and I am learning it in simple example app.
My current set up is 3 rootViewControlers: LoadingStateCoordinator, WelcomeCoordinator, TabBarCoordinator but missing connection between UIKit and coordinators. I am trying to implement it with a UINavigationController but the button is not being called. I need a way to connect to back button and a reusable coordinator that I could push to and dellocate accordingly (that is without RxSwift).*Set up Welcome screen as the parent/main navigation and always be able to come back to it.**
So after user selects a form from modal view (vertical flow) presented I show on a push a TabBarCoordinator (horizontal). All viewControllers have empty.storyboard, UIViewController and Coordinator exept the TabBar.Here I only have a coordinator due to the set up of child tab coordinators and the magic needs to happen on a back button tap. Currenly this only being called when user comes from LoadingStateCoordinator. There I need to send the user back to the Welcome screen so they can change the onboarding set up. Here is the first code for LoadingStateCoordinator:
final class LoadingStateCoordinator: NSObject, Coordinator {
*// MARK: - Inputs required*
var childCoordinators: [Coordinator]
var presenter: UINavigationController
private let window: UIWindow
*// MARK: - Initialization*
init(window: UIWindow) {
self.window = window
childCoordinators = []
presenter = UINavigationController()
}
*// MARK: - Coordinator*
func start() {
let controller: LoadingStateViewController = LoadingStateViewController.instantiate()
window.rootViewController = controller
controller.delegate = self
}
}
*// MARK: - LoadingViewControllerDelegate*
extension LoadingStateCoordinator : LoadingViewControllerDelegate {
func performScreenSwitch() {
if UserDefaults.standard.userWasHere == false {
let tabCoordinator: TabBarCoordinator = TabBarCoordinator(window: window, tabBarController: UITabBarController())
window.rootViewController = presenter
addChildCoordinator(tabCoordinator)
tabCoordinator.start()
presenter.pushViewController(tabCoordinator.tabBarController!, animated: true)
} else {
let welcomeCoordinator = WelcomeCoordinator(window: window, presenter: presenter)
window.rootViewController = welcomeCoordinator.presenter
addChildCoordinator(welcomeCoordinator)
welcomeCoordinator.start()
}
}
}
And here is the TabBarCoordinator that need to perform back to Welcome screen action. When I present popToRootfunction it pushes the Welcome screen but all the button there are disbled. I guess to be retain cycle issue. Do I need funadametally another set up? Is there a way to popToRoot(vc) in this set up? What I tryed ended with runtime error "poping to non existing controller".
TabBarCoordinator code that need to perform this:
final class TabBarCoordinator: NSObject, Coordinator {
internal var presenter: UINavigationController
internal var tabBarController: UITabBarController?
internal var childCoordinators: [Coordinator]
var parentCoordinator: LoadingStateCoordinator?
lazy var leftBtn: UIBarButtonItem = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "arrow.turn.up.left"), for: .normal)
button.sizeToFit()
button.addTarget(self,
action: #selector(self.popToRoot(_:)),
for: .touchUpInside)
return UIBarButtonItem(customView: button)
}()
init(window: UIWindow, tabBarController: UITabBarController) {
self.tabBarController = tabBarController
childCoordinators = []
self.presenter = UINavigationController()
}
func start() {
performGetTabBar()
self.presenter.delegate = self
}
private func performGetTabBar() {
let coordinators: [Coordinator] = generateTabCoordinators()
coordinators.forEach({ coordinator in
coordinator.start()
addChildCoordinator(coordinator)
})
let presenters: [UIViewController] = coordinators.map({ coordinator -> UIViewController in
return coordinator.presenter
})
leftBtn.style = .plain
tabBarController?.navigationItem.leftBarButtonItem = leftBtn
tabBarController?.setViewControllers(presenters, animated: false)
selectTab(type: SurfTripCoordinator.self)
}
private func generateTabCoordinators() -> [Coordinator] {
let calculatorCoordinator: CalculatorCoordinator = CalculatorCoordinator(presenter: UINavigationController())
let tripCoordinator: SurfTripCoordinator = SurfTripCoordinator(presenter: UINavigationController())
let sellCoordinator: SavedTripsCoordinator = SavedTripsCoordinator(presenter: UINavigationController())
return [calculatorCoordinator, tripCoordinator, sellCoordinator]
}
*//this is not being called when coming from vertical flow*
#objc func popToRoot(_ sender: UIBarButtonItem) {
let storyboard: UIStoryboard = UIStoryboard(name: Constants.Storyboards.welcomeViewCoordinator, bundle: nil)
let controller: WelcomeViewController = WelcomeViewController.instantiate(from: storyboard)
tabBarController?.navigationController?.pushViewController(controller, animated: true)
}
}
extension TabBarCoordinator: UINavigationControllerDelegate {
func selectTab<T: Coordinator>(type _: T.Type) {
guard let index = childCoordinators.firstIndex(where: { coordinator in
coordinator is T
}) else {
return
}
tabBarController?.selectedIndex = index
}
}
and here is the current WelcomeCoordinator set up
class WelcomeCoordinator: NSObject, Coordinator {
internal var presenter: UINavigationController
var childCoordinators: [Coordinator]
init(window: UIWindow, presenter: UINavigationController) {
self.presenter = presenter
childCoordinators = []
}
func start() {
let storyboard: UIStoryboard = UIStoryboard(name: Constants.Storyboards.welcomeViewCoordinator, bundle: nil)
let controller: WelcomeViewController = WelcomeViewController.instantiate(from: storyboard)
controller.delegate = self
presenter.pushViewController(controller, animated: true)
}
}
extension WelcomeCoordinator : WelcomeViewControllerDelegate {
func performAddLevel() {
let addLevelCoordinator: AddLevelViewCoordinator = AddLevelViewCoordinator(presenter: UINavigationController())
addLevelCoordinator.start()
addChildCoordinator(addLevelCoordinator)
addLevelCoordinator.presenter.modalPresentationStyle = .fullScreen
presenter.present(addLevelCoordinator.presenter, animated: true, completion: nil)
}
}
sorry for the long post I wish there was more reaktive native way to do this...
Ok so I found partlly a solution the back button solution for my case: not using pushViewController or show because it comes with back button. presenter.setViewControllers([tabCoordinator.tabBarController!], animated: true) and there setting the navBar to hidden. I made my own navItem button to navigate to rootVC. Next step to allocate and remove all child tabBar coordinators on back tap recognized.
I have an app that uses a UITabBarController with 7 Tabs. Each tab is a UIViewController subclass (each embedded in a UINavigationController) that simply has a different background color in the view set in the storyboard. The TabItems are labeled Tab 1 through Tab 7 and the Title set in each NavBar is simply the Tab number. I've added some static Quick Actions in my Info.plist that allow me to jump to Tab 2, Tab 3, Tab 6 and Tab 7.
The problem I run into is when I set the selected Tab when handling the quick action in the AppDelegate all works fine for the first 4 tabs. If I select one of the Tabs listed in the More... list the app opens up only selecting the first tab in my UITabBarController. However if the app had already been running and I go to the home screen and try the Quick Action again then it is able to select any of the tabs from the More list. Any ideas?
Here is my AppDelegate code:
// AppDelegate.swift
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// handle quick actions
if let shortcutItem =
launchOptions?[UIApplicationLaunchOptionsKey.shortcutItem]
as? UIApplicationShortcutItem {
let _ = handleShortcut(shortcutItem: shortcutItem)
return false
}
return true
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
completionHandler(handleShortcut(shortcutItem: shortcutItem))
}
private func handleShortcut(shortcutItem: UIApplicationShortcutItem) -> Bool {
let shortcutType = shortcutItem.type
guard let shortcutIdentifier = ShortcutIdentifier(fullIdentifier: shortcutType) else {
return false
}
switch shortcutIdentifier {
case ShortcutIdentifier.OpenTab2: fallthrough
case ShortcutIdentifier.OpenTab3: fallthrough
case ShortcutIdentifier.OpenTab6: fallthrough
case ShortcutIdentifier.OpenTab7:
return selectTabBarItem(forIdentifier: shortcutIdentifier)
}
}
private func selectTabBarItem(forIdentifier identifier: ShortcutIdentifier) -> Bool {
if let tabBarController = self.window?.rootViewController as? CustomTabBarController
{
switch (identifier)
{
case .OpenTab2:
tabBarController.selectedIndex = tabDictionary["OpenTab2"]!
case .OpenTab3:
tabBarController.selectedIndex = tabDictionary["OpenTab3"]!
case .OpenTab6:
tabBarController.selectedIndex = tabDictionary["OpenTab6"]!
case .OpenTab7:
tabBarController.selectedIndex = tabDictionary["OpenTab7"]!
}
}
return true
}
// Integer in dictionary denotes tab number (zero based)
private let tabDictionary = ["OpenTab2": 1, "OpenTab3": 2, "OpenTab6": 5, "OpenTab7": 6]
enum ShortcutIdentifier: String {
case OpenTab2
case OpenTab3
case OpenTab6
case OpenTab7
init?(fullIdentifier: String) {
guard let shortIdentifier = fullIdentifier.components(separatedBy: ".").last else {
return nil
}
self.init(rawValue: shortIdentifier)
}
}
}
I found the following link (http://jakzaprogramowac.pl/pytanie/18417,select-index-greater-than-3-for-uitabbarcontroller-at-app-launch-not-working) that seemed to answer my question. I hope this helps others.
I added the following code to the beginning of AppDelegate DidFinishLaunchingWithOptions to initially select the first tab and call layoutIfNeeded() on the tabBarController's View:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch.
// initially select first tab so more selection works from quick actions - fixup
if let tabBarController = self.window?.rootViewController as? CustomTabBarController
{
tabBarController.selectedIndex = 0
tabBarController.view.layoutIfNeeded()
}
// handle quick actions
if let shortcutItem =
launchOptions?[UIApplicationLaunchOptionsKey.shortcutItem]
as? UIApplicationShortcutItem {
let _ = handleShortcut(shortcutItem: shortcutItem)
return false
}
return true
}
In Objective-C, I would normally use something like this:
static NSString *kViewTransformChanged = #"view transform changed";
// or
static const void *kViewTransformChanged = &kViewTransformChanged;
[clearContentView addObserver:self
forKeyPath:#"transform"
options:NSKeyValueObservingOptionNew
context:&kViewTransformChanged];
I have two overloaded methods to choose from to add an observer for KVO with the only difference being the context argument:
clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, context: CMutableVoidPointer)
clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, kvoContext: KVOContext)
With Swift not using pointers, I'm not sure how to dereference a pointer to use the first method.
If I create my own KVOContext constant for use with the second method, I wind up with it asking for this:
let test:KVOContext = KVOContext.fromVoidContext(context: CMutableVoidPointer)
EDIT: What is the difference between CMutableVoidPointer and KVOContext? Can someone give me an example how how to use them both and when I would use one over the other?
EDIT #2: A dev at Apple just posted this to the forums: KVOContext is going away; using a global reference as your context is the way to go right now.
There is now a technique officially recommended in the documentation, which is to create a private mutable variable and use its address as the context.
(Updated for Swift 3 on 2017-01-09)
// Set up non-zero-sized storage. We don't intend to mutate this variable,
// but it needs to be `var` so we can pass its address in as UnsafeMutablePointer.
private static var myContext = 0
// NOTE: `static` is not necessary if you want it to be a global variable
observee.addObserver(self, forKeyPath: …, options: [], context: &MyClass.myContext)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if context == &myContext {
…
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
Now that KVOContext is gone in Xcode 6 beta 3, you can do the following. Define a global (i.e. not a class property) like so:
let myContext = UnsafePointer<()>()
Add an observer:
observee.addObserver(observer, forKeyPath: …, options: nil, context: myContext)
In the observer:
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafePointer<()>) {
if context == myContext {
…
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
Swift 4 - observing contentSize change on UITableViewController popover to fix incorrect size
I had been searching for an answer to change to a block based KVO because I was getting a swiftlint warning and it took me piecing quite a few different answers together to get to the right solution. Swiftlint warning:
Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo).
My use case was to present a popover controller attached to a button in a Nav bar in a view controller and then resize the popover once it's showing - otherwise it would be too big and not fitting the contents of the popover. The popover itself was a UITableViewController that contained static cells, and it was displayed via a Storyboard segue with style popover.
To setup the block based observer, you need the following code inside your popover UITableViewController:
// class level variable to store the statusObserver
private var statusObserver: NSKeyValueObservation?
// Create the observer inside viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusObserver = tableView.observe(\UITableView.contentSize,
changeHandler: { [ weak self ] (theTableView, _) in self?.popoverPresentationController?.presentedViewController.preferredContentSize = theTableView.contentSize
})
}
// Don't forget to remove the observer when the popover is dismissed.
override func viewDidDisappear(_ animated: Bool) {
if let observer = statusObserver {
observer.invalidate()
statusObserver = nil
}
super.viewDidDisappear(animated)
}
I didn't need the previous value when the observer was triggered, so left out the options: [.new, .old] when creating the observer.
Update for Swift 4
Context is not required for block-based observer function and existing #keyPath() syntax is replaced with smart keypath to achieve swift type safety.
class EventOvserverDemo {
var statusObserver:NSKeyValueObservation?
var objectToObserve:UIView?
func registerAddObserver() -> Void {
statusObserver = objectToObserve?.observe(\UIView.tag, options: [.new, .old], changeHandler: {[weak self] (player, change) in
if let tag = change.newValue {
// observed changed value and do the task here on change.
}
})
}
func unregisterObserver() -> Void {
if let sObserver = statusObserver {
sObserver.invalidate()
statusObserver = nil
}
}
}
Complete example using Swift:
//
// AppDelegate.swift
// Photos-MediaFramework-swift
//
// Created by Phurg on 11/11/16.
//
// Displays URLs for all photos in Photos Library
//
// #see http://stackoverflow.com/questions/30144547/programmatic-access-to-the-photos-library-on-mac-os-x-photokit-photos-framewo
//
import Cocoa
import MediaLibrary
// For KVO: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID12
private var mediaLibraryLoaded = 1
private var rootMediaGroupLoaded = 2
private var mediaObjectsLoaded = 3
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mediaLibrary : MLMediaLibrary!
var allPhotosAlbum : MLMediaGroup!
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSLog("applicationDidFinishLaunching:");
let options:[String:Any] = [
MLMediaLoadSourceTypesKey: MLMediaSourceType.image.rawValue, // Can't be Swift enum
MLMediaLoadIncludeSourcesKey: [MLMediaSourcePhotosIdentifier], // Array
]
self.mediaLibrary = MLMediaLibrary(options:options)
NSLog("applicationDidFinishLaunching: mediaLibrary=%#", self.mediaLibrary);
self.mediaLibrary.addObserver(self, forKeyPath:"mediaSources", options:[], context:&mediaLibraryLoaded)
NSLog("applicationDidFinishLaunching: added mediaSources observer");
// Force load
self.mediaLibrary.mediaSources?[MLMediaSourcePhotosIdentifier]
NSLog("applicationDidFinishLaunching: done");
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
NSLog("observeValue: keyPath=%#", keyPath!)
let mediaSource:MLMediaSource = self.mediaLibrary.mediaSources![MLMediaSourcePhotosIdentifier]!
if (context == &mediaLibraryLoaded) {
NSLog("observeValue: mediaLibraryLoaded")
mediaSource.addObserver(self, forKeyPath:"rootMediaGroup", options:[], context:&rootMediaGroupLoaded)
// Force load
mediaSource.rootMediaGroup
} else if (context == &rootMediaGroupLoaded) {
NSLog("observeValue: rootMediaGroupLoaded")
let albums:MLMediaGroup = mediaSource.mediaGroup(forIdentifier:"TopLevelAlbums")!
for album in albums.childGroups! {
let albumIdentifier:String = album.attributes["identifier"] as! String
if (albumIdentifier == "allPhotosAlbum") {
self.allPhotosAlbum = album
album.addObserver(self, forKeyPath:"mediaObjects", options:[], context:&mediaObjectsLoaded)
// Force load
album.mediaObjects
}
}
} else if (context == &mediaObjectsLoaded) {
NSLog("observeValue: mediaObjectsLoaded")
let mediaObjects:[MLMediaObject] = self.allPhotosAlbum.mediaObjects!
for mediaObject in mediaObjects {
let url:URL? = mediaObject.url
// URL does not extend NSObject, so can't be passed to NSLog; use string interpolation
NSLog("%#", "\(url)")
}
}
}
}