Implement WatchConnectivity in an iOS App that supports iOS 8.0 - swift

My current iOS App supports iOS 8 and greater. I decided to implement a Watch App and I want to have watch and iOS app comunicate via WatchConnectivity. Let's say that I want to build a class to handle the connectivity and this class is a WCSessionDelegate. To be able to include the class in my project I've to wrap it into the #available(iOS 9.0,0) tag and that's ok...but, how can I keep track of an instance of this class in the AppDelegate (or in any other class)???
I cannot write something like
class AppDelegate: UIResponder, UIApplicationDelegate {
#available(iOS 9.0,0)
var connectivityHandler: ConnectivityHandler?
How can I use these class in a project that should support also iOS 8?!
Edit----
I've found this solution but it doesn't sound really "good". I create a dynamic property and dependently on the current system I return a instance or just nil... the property should obviously be of type AnyObject to be a valid iOS8 parameter...
var _connectivityHandler: AnyObject?
var connectivityHandler: AnyObject? {
get {
if _connectivityHandler != nil {
return _connectivityHandler
}else{
if #available(iOS 9.0, *) {
_connectivityHandler = ConnectivityHandler()
} else {
_connectivityHandler = nil
}
return _connectivityHandler
}
}
set(value) {
_connectivityHandler = value
}
}

You can move all the WatchConnectivity functions in your connectivityHandler to an extension of that class and mask that with the #availability API:
import UIKit
import WatchConnectivity
class ConnectivityHandler: NSObject {
}
#available(iOS 9.0, *)
extension ConnectivityHandler: WCSessionDelegate {
func initSession() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
}
If you do that you can add this class to your AppDelegate and it also compiles in iOS 8:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let connectivityHandler = ConnectivityHandler()
....
}

Related

Hiding Edit Menu of a SwiftUI / MacOS app

My MacOS app doesn't have any text editing possibilities. How can I hide the Edit menu which is added to my app automatically? I'd prefer to do this in SwiftUI.
I would expect the code below should work, but it doesn't.
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
CommandGroup(replacing: .textEditing) {}
}
}
}
To my knowledge you cannot hide the whole menu, you can just hide element groups inside of it:
.commands {
CommandGroup(replacing: .pasteboard) { }
CommandGroup(replacing: .undoRedo) { }
}
For native (Cocoa) apps
It is possible to remove application menus using an NSApplicationDelegate. This approach may break in future macOS versions (e.g. if the position of the Edit menu were to change), but does currently work:
class MyAppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
let indexOfEditMenu = 2
func applicationDidFinishLaunching(_ : Notification) {
NSApplication.shared.mainMenu?.removeItem(at: indexOfEditMenu)
}
}
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
For Catalyst (UIKit) apps
For Catalyst-based macOS apps, the approach is similar to that above, except that a UIApplicationDelegate deriving from UIResponder is used:
class MyAppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
override func buildMenu(with builder: UIMenuBuilder) {
/// Only operate on the main menu bar.
if builder.system == .main {
builder.remove(menu: .edit)
}
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
The current suggestions failed for me when SwiftUI updated the body of a window.
Solution:
Use KVO and watch the NSApp for changes on \.mainMenu. You can remove whatever you want after SwiftUI has its turn.
#objc
class AppDelegate: NSObject, NSApplicationDelegate {
var token: NSKeyValueObservation?
func applicationDidFinishLaunching(_ notification: Notification) {
// Remove a single menu
if let m = NSApp.mainMenu?.item(withTitle: "Edit") {
NSApp.mainMenu?.removeItem(m)
}
// Remove Multiple Menus
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Must remove after every time SwiftUI re adds
token = NSApp.observe(\.mainMenu, options: .new) { (app, change) in
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Remove a single menu
guard let menu = app.mainMenu?.item(withTitle: "Edit") else { return }
app.mainMenu?.removeItem(menu)
}
}
}
struct MarblesApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some View {
//...
}
}
Thoughts:
SwiftUI either has a bug or they really don't want you to remove the top level menus in NSApp.mainMenu. SwiftUI seems to reset the whole menu with no way to override or customize most details currently (Xcode 13.4.1). The CommandGroup(replacing: .textEditing) { }-esque commands don't let you remove or clear a whole menu. Assigning a new NSApp.mainMenu just gets clobbered when SwiftUI wants even if you specify no commands.
This seems like a very fragile solution. There should be a way to tell SwiftUI to not touch the NSApp.mainMenu or enable more customization. Or it seems SwiftUI should check that it owned the previous menu (the menu items are SwiftUI.AppKitMainMenuItem). Or I'm missing some tool they've provided. Hopefully this is fixed in the WWDC beta?
(In Xcode 13.4.1 with Swift 5 targeting macOS 12.3 without Catalyst.)
For those of you that are looking for any updates on this - have a look at this question that I asked (and answered myself):
SwiftUI Update the mainMenu [SOLVED] kludgey
The way I got around it was to put it in a DispatchQueue.main.async closure in the AppDelegate applicationWillUpdate function
import Foundation
import AppKit
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationWillUpdate(_ notification: Notification) {
DispatchQueue.main.async {
let currentMainMenu = NSApplication.shared.mainMenu
let editMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "Edit")
if nil != editMenu {
NSApp.mainMenu?.removeItem(editMenu!)
}
}
}
}
It took me a good 4 days of searching and attempting things :) - typical that it came down to a 2 line code change

Functions of WKExtensionDelegate are not called for watchOS

This is my WatchDelegate.swift:
import WatchKit
class WatchDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
print("FINISHED LAUNCHING>>>>>>>>>>>") //not called
}
}
My info.plist:
My app is launching on my watch, but above delegate method is not called. Why?
In my case I didn't add anything to info.plist, I just added some magic swift sprinkles:
import SwiftUI
#main
struct WatchApp: App {
// 👇👇👇
#WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate: ExtensionDelegate
// 👆👆👆
var body: some Scene {
WindowGroup {
// etc
I then see logging from
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// stuff logged here shows in console
}
}

private func: Missing argument for parameter #1 in call

Well, here's another 'Missing argument for parameter #1 in call' issue. (Seems Apple could do a better job naming its errors :-p )
In my class, I'm calling a private function libraryVisibility(), and on that line I get a Missing argument for parameter #1 in call error on compilation. Don't really understand why.
Anyhow, here's the code:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var libraryState: Bool = libraryVisibility()
func applicationDidFinishLaunching(aNotification: NSNotification) {
…
}
private func libraryVisibility() -> Bool {
…
// dostuff and return boolean
return true
}
}
You can't call instance functions in the default initialiser of a property
You can either make your libraryVisibility() function a class function:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var libraryState : Bool = AppDelegate.libraryVisibility()
private class func libraryVisibility() -> Bool {
let homeUrl = NSURL(string: NSHomeDirectory())
let libraryUrl = NSURL(string: "Library", relativeToURL: homeUrl)
return libraryUrl!.hidden
}
}
Or you could make your libraryState property a lazy property:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
lazy var libraryState : Bool = self.libraryVisibility()
private func libraryVisibility() -> Bool {
let homeUrl = NSURL(string: NSHomeDirectory())
let libraryUrl = NSURL(string: "Library", relativeToURL: homeUrl)
return libraryUrl!.hidden
}
}
The property will then get initialised the first time you use it.
Mike Buss has a nice usage guide on how to use lazy variables.
Ok, I have solved it, but not entirely sure why this is working:
Giving the libraryState variable a default value and setting it inside applicationDidFinishLaunching seems to do the trick:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var libraryState: Bool = false
func applicationDidFinishLaunching(aNotification: NSNotification) {
libraryState = libraryVisibility()
}
}

swift share information between two classes

I have a tabbed view app that I am working on right now I am wondering if there's any way I can share the information from my FirstViewController.swift to my SecondViewController.swift? Since I know swift doesnt support multiple class inheritance, is there a way that I can use the variables and informations I have on my FirstViewController in my SecondViewController?
You'll need a model outside the context of both your view controllers to store data, and then a way to populate those view controllers with that data and provide a reference to the model to make changes from the view controllers. You could this by creating a new model object that conforms to UITabBarColtronnerDelegate, which will allow you access to a view controller after it is selected. Keep things decoupled by abstracting these relationships into protocols, which then allows your implementations to vary independently as your app scales up.
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let applicationModel = ApplicationModel()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if let tabBarController = window?.rootViewController as? UITabBarController {
tabBarController.delegate = applicationModel
}
return true
}
}
protocol TabbedViewControllerDelegate {
var message: String { set get }
}
class ApplicationModel: NSObject, UITabBarControllerDelegate, TabbedViewControllerDelegate {
var message = "Hello World!"
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if var tabbedViewController = viewController as? TabbedViewController {
tabbedViewController.message = message
tabBarController.delegate = self
}
}
}
protocol TabbedViewController {
var message: String { set get }
var delegate: TabbedViewControllerDelegate? { get set }
}
class FirstViewController: UIViewController, TabbedViewController {
var delegate: TabbedViewControllerDelegate?
var message: String = "" {
didSet {
println( "FirstViewController populated with message: \(message)" )
}
}
#IBAction func buttonPressed( sender: AnyObject? ) {
self.delegate?.message = "Updated message from FirstViewController"
}
}
class SecondViewController: UIViewController, TabbedViewController {
var delegate: TabbedViewControllerDelegate?
var message: String = "" {
didSet {
println( "SecondViewController populated with message: \(message)" )
}
}
#IBAction func buttonPressed( sender: AnyObject? ) {
self.delegate?.message = "Updated message from SecondViewController"
}
}

How to mock UIApplication in Swift?

I'm currently using Quick + Nimble for my unit testing in Swift. I'm building an Inviter class that sends app invites via different methods.
I need to mock out UIApplication to verify that my code calls openURL.
My code so far:
import Quick
import Nimble
import OCMock
extension Inviter {
convenience init(usingMockApplication mockApplication: UIApplication) {
self.init()
application = mockApplication
}
}
class MockUIApplication : UIApplication {
var application = UIApplication.sharedApplication()
var openedURL: String?
override func openURL(url: NSURL) -> Bool {
openedURL = url.absoluteString
return true
}
}
class InviterSpec: QuickSpec {
override func spec() {
describe("Inviter") {
var mockApplication = MockUIApplication()
var inviter = Inviter(usingMockApplication: mockApplication)
beforeEach() {
inviter = Inviter(usingMockApplication: mockApplication)
}
context("for WhatsApp invites") {
beforeEach() {
inviter.inviteViaWhatsAppWithMessage("Invite Message.")
}
it("should tell the application to open WhatsApp") {
expect(mockApplication.openedURL).toNot(beNil())
}
it("should send WhatsApp the right message") {
let message = mockApplication.openedURL?.lastPathComponent
expect(message).to(equal("Invite%Message."))
}
}
}
}
}
In this approach, my app errors at runtime stating there can understandably be only one UIApplication. Previously, one could make MockUIApplication inherit from NSObject, and pass that in. Unfortunately Swift's strict type checking seems to prevent that too.
Would love any ideas.
You are close. Use a protocol for the functions you need.
protocol UIApplicationProtocol {
func openURL(url: NSURL) -> Bool
}
extension UIApplication: UIApplicationProtocol {}
Then you just need to use the protocol instead of the class
extension Inviter {
convenience init(usingMockApplication mockApplication: UIApplicationProtocol) {
self.init()
application = mockApplication
}
}
You will need to modify the Inviter class to use UIApplicationProtocol as well.