Swift 5 library to easily set up a MVC design pattern with any SwiftUI App
SwiftUI makes it very easy to set up a proper Model-View-Controler pattern for apps. But doing so requires a lot of boilerplate code for every new app. So, this library contains all that in order to get startet with the real app as quick as possible.
- Multi Window Apps on iOS & macCatalyst
- Controller classes per App instance and per Window
- Error Handling
- Recent Files
- App Menus
- Open Files Dialog
- View classes to make live easier
- Drop Zones for Drang & Drop
- Select & Multiselect items
- In-App Notificatoins (to confirm user-initiatedactions)
With the swift package manager, add the library to your dependencies
dependencies: [
.package(url: "https://github.com/brampf/swiftmvc.git", from: "0.1.0")
]
then simply add the MVC
import to your target
.target(name: "YourApp", dependencies: ["MVC"])
Add the packet to your Xcode project and Xcode will automatically import the library
Since SwiftMVC handles the whole Scene configuration stuff, make sure that the info.plist does only contain the UIApplicationSupportsMultipleScenes
key and nothing else. Otherwise make sure, that the scenes in there match the scenes defined in your App delegate (see below)
A naked project should look like this:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
As a minimum, you need to create 3 swift files:
- The
AppController
which needs to be anObservalbeObject
- The
WindowController
which also needs to be anObservableObject
- The
RootView
of your App which is an SwiftUIView
The controller will automatically be injected as EnvironmentObjects
in the view, the AppController
as shared instance between all windows and WindowController
with seperate instance per window in a multi-window-environment.
Now, set up the whole SwiftMVC app in your AppDelegate by adding the inheritance from SceneConfiguration
as well as the ApplicationDelegate
. This will make sure, that the library injects the proper scene configuration and window controllers in your App
import MVC
import UIKit
@UIApplicationMain
class AppDelegate: ApplicationDelegate<AppController>, UIApplicationDelegate, SceneConfiguration {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
// return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
self.sceneForActivity(activity: options.userActivities.first?.activityType ?? "")
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
var scenes: Set<SceneSetup> {
return .init(arrayLiteral: self.mainScene)
}
var mainScene: SceneSetup = SceneSetup("swiftMVC.app.default", name: "Default Configuration", role: .windowApplication, delegate: WindowSceneDelegate<DemoApp.WindowController, DemoApp.AppController, DemoApp.WindowRootView>.self)
}
That's it. Build & Launch the app
The WindowController
handles errors as well as opening files the following code will open an dialog asking for one image
Button("OPEN"){
let picker = DocumentPickerViewController(false, UTIHandler("public.image", onError: self.windowControl.publishFail(_:), onOpen: .URL(onOpen: self.windowControl.open(url: ))))
if let window = self.windowControl.window?() {
picker.show(window: window)
} else {
picker.show()
}
}
}
Alternatively, you can use the DropArea
View to simply drang & drop files onto it
DropArea(UTIHandler("public.image", onError: self.windowControl.publishFail(_:), onOpen: .URL(onOpen: self.windowControl.open(url: ))))
Both use the same UTIHandler
strucutre which will will call the given onOpen
method for each file provided.
Errors and error messages are handeled as Fail
via the publishFail
methods in the Controller classes. In order to present it, just use the FailSummary
View, which will set the binding to nil
when closed via the "x"
.popover(item: self.$windowControl.fail) { fail in
FailSummary(item: self.$windowControl.fail)
}
Sometimes it comes in handy to show some information about your app via the AppInfo
View:
AppInfo(Image(systemName: "gear"), name: AppDelegate.appName, version: AppDelegate.appVersion, build: AppDelegate.buildNumber)
Especially with key controls, it's sometimes nice to confirm user interactions with a small dialog which will vanish by itself. This can be set as NotificationView
on the WindowController
First, add the PopupNotification
in the View you like
ZStack{
// your View goes here
if windowControl.note != nil {
PopupNotification(self.$windowControl.note)
}
}
All you then have to do, is trigger the notification for user interaction like clicking a button:
Button("Notification"){
self.windowControl.note = NotificationView(.LONG, image: Image(systemName: "lightbulb"), title: "Notification", subtitle: "Confirm user interaction")
}
MIT license; see LICENSE. (c) 2020 Max Schroeder