MVC for SwiftUI (yes, seriously)

Overview

ViewController

ViewController's for SwiftUI.

The core idea is that the ViewController is owning, or at least driving, the View(s). Not the other way around.

Blog entry explaining all the things: Model View Controller for SwiftUI

Quick: How to Use

Just the basics to get started quickly.

Step A: Setup Project and Root VC

  • create a SwiftUI project in Xcode (iOS is tested better)
  • add the ViewController package, e.g. via [email protected]:ZeeZide/ViewController.git
  • create a new RootViewController, e.g. HomePage.swift:
    import ViewController
    
    class HomePage: ViewController {
      
      var view: some View {
        VStack {
          Text("Welcome to MWC!")
            .font(.title)
            .padding()
          
          Spacer()
        }
      }
    }
  • Instantiate that in the scene view, the ContentView.swift generated by Xcode:
    import ViewController
    
    struct ContentView: View {
      var body: some View {
        MainViewController(HomePage())
      }
    }
  • Compile and Run, should show the HomePage

Step B: Add a presented VC and navigate to it

  • create a new ViewController, e.g. Settings.swift:
    import ViewController
    
    class Settings: ViewController {
      
      var view: some View { // the View being controlled
        VStack {
          Text("Welcome to Settings!")
            .font(.title)
            .padding()
          
          Spacer()
        }
      }
    }
  • Add an action to present the Settings from the HomePage:
    import ViewController
    
    class HomePage: ViewController {
    
      func configureApp() {
        show(Settings()) // or `present(Settings())`
      }
      
      var view: some View {
        VStack {
          Text("Welcome to MWC!")
            .font(.title)
            .padding()
            
          Divider()
          
          Button(action: self.configureApp) {
            Label("Configure", systemImage: "gear")
          }
          
          Spacer()
        }
      }
    }

Pressing the button should show the settings in a sheet.

Step C: Add a NavigationController for Navigation :-)

  • Wrap the HomePage in a NavigationController, in the scene view:
    import ViewController
    
    struct ContentView: View {
      var body: some View {
        MainViewController(NavigationController(rootViewController: HomePage()))
      }
    }

Note pressing the button does a navigation. Things like this should also work:

func presentInSheet() {
  let vc = SettingsPage()
  vc.modalPresentationStyle = .sheet
  present(vc)
}

Adding a PushLink

The presentations so far make use of a hidden link. To explicitly inline a NavigationLink, use PushLink, which wraps that.

  • Add a PushLink (until I get an NavigationLink init extension working) to present the Settings from the HomePage:
    import ViewController
    
    class HomePage: ViewController {
      
      var view: some View {
        VStack {
          Text("Welcome to MWC!")
            .font(.title)
            .padding()
            
          Divider()
          
          PushLink("Open Settings", to: Settings())
          
          Spacer()
        }
      }
    }

Who

ViewController is brought to you by ZeeZide. We like feedback, GitHub stars, cool contract work, presumably any form of praise you can think of.

Want to support my work? Buy an app: Past for iChat, SVG Shaper, Shrugs, HMScriptEditor. You don't have to use it! 😀

Comments
  • Type mismatch error when dismissing a sheet

    Type mismatch error when dismissing a sheet

    When a sheet is dismissed, the content of the sheet sometimes goes away too soon (and is replaced with the builtin, red, type mismatch error panel). Likely because the presented object is removed before the sheet gets dismissed. Navigation doesn't seem to have the issue.

    So need to find a way to hang on to the presented VC a little longer. Maybe keep it around as a "disappearingVC" and mark it gone once didDisappear fires? (auto-sheet presentation only).

    bug 
    opened by helje5 1
  • Maybe drop the

    Maybe drop the "custom presentations" (`presentInSheet`/`presentInNavigation`)

    Presumably only auto is going to be used going forward. They are kinda left-overs approaching the auto-goal.

    We can still keep the .custom presentation mode, and the user can manually bind the the presented VC and present however he likes.

    opened by helje5 1
  • The swift crasher for the DefaultViewControllerView might be fixed

    The swift crasher for the DefaultViewControllerView might be fixed

    @DivineDominion says that:

    I believe that the segfault is gone in Xcode 13.4 (at least I don't see any when compiling)

    For this issue: https://github.com/ZeeZide/ViewController/blob/develop/Sources/ViewController/ViewControllerView.swift#L28

    Should #ifdef the release and enable the smarter debug default view.

    opened by helje5 0
  • Support binds against `@Published` properties

    Support binds against `@Published` properties

    This is a funny side effect when using the inline view accessor like so:

    class CowsOverview: ViewController {
      @Published var search = ""
      var view: some View {
        TextField("Search it", $search) // <= doesn't work
      }
    

    This does work when using the ViewController via the environment, e.g.:

    class CowsOverview: ViewController {
      @Published var search = ""
      struct ContentView: View {
        @EnvironmentObject var viewController : CowsOverview
        TextField("Search it", $viewController.search)
      }
    }
    

    This is because the $ accesses the projectedValue of the property wrapper - @Published in the first case, and @EnvironmentObject in the second. And while @EnvironmentObject gives you a Binding (and then that goes on traversing keypathes), the @Published gives you the Combine Publisher for the property.

    There are a few ways to solve this, either using a trampoline providing the Bindings, maybe like:

    var `$`: Trampoline<Self, ... key path stuff>
    

    or maybe just adding a .bind() to @Published, like:

    extension Published {
      var bind : Binding<Value> { ... }
    }
    

    Maybe someone has additional ideas. Generally it is not a huge deal, as one often ends up w/ real view structs anyways.

    enhancement 
    opened by helje5 3
  • Add a `SplitViewController`

    Add a `SplitViewController`

    For common master/detail setups. A SwiftUI NavigationView really is a UI/NSSplitView already :-) But the semantics wrt to show and showDetail would be different.

    Implementation shouldn't be too hard, depending on how many features are to be replicated.

    A first attempt, to be finished:

    /**
     * Type erased version of the ``SplitViewController``. Check that for more
     * information.
     */
    public protocol _SplitViewController: _ViewController {
      
      typealias Style  = SplitViewControllerStyle
      typealias Column = SplitViewControllerColumn
      
    }
    
    public enum SplitViewControllerStyle: Equatable {
      case doubleColumn
      case tripleColumn
    }
    
    public enum SplitViewControllerColumn: Equatable {
      case primary
      case supplementary
      case secondary
    }
    
    
    /**
     * A simple wrapper around SwiftUI's `NavigationView`.
     *
     * Should be used as a root only.
     *
     * This adds a few `UISplitViewController` like behaviour, but in the end just
     * hooks into `NavigationView`
     * (which is a SplitViewController in wider layouts).
     *
     * Unlike `UISplitViewController`, this does not wrap the children in
     * `NavigationController`s (this is handled by SwiftUI itself).
     *
     * Example:
     * ```swift
     * struct ContentView: View { // the "scene view"
     *
     *   var body: some View {
     *     MainViewController(SplitViewController(style: .doubleColumn))
     *   }
     * }
     * ```
     *
     * Note that this works quite differently to a `UISplitViewController`.
     *
     * 2022-04-25: Note that programmatic navigation in SwiftUI is still a mess,
     *             i.e. popping in a 3-pane controller may fail.
     */
    open class SplitViewController: ViewController, _SplitViewController {
      // TBD: We could probably make this more typesafe if we tie it to three
      //      columns?
      
      @Published public var style           : SplitViewControllerStyle
      @Published public var viewControllers : [ AnyViewController ]
    
      init(style: SplitViewControllerStyle = .doubleColumn,
           viewControllers: [ AnyViewController ] = [])
      {
        self.style           = style
        self.viewControllers = viewControllers
      }
      
      convenience
      public init<PrimaryVC, SupplementaryVC, SecondaryVC>(
        _ primary       : PrimaryVC,
        _ supplementary : SupplementaryVC,
        _ secondary     : SecondaryVC
      ) where PrimaryVC       : ViewController,
              SupplementaryVC : ViewController,
              SecondaryVC     : ViewController
      {
        self.init(style: .tripleColumn, viewControllers: [
          AnyViewController(primary),
          AnyViewController(supplementary),
          AnyViewController(secondary)
        ])
        addChild(primary)
        addChild(supplementary)
        addChild(secondary)
      }
      convenience
      public init<PrimaryVC, SecondaryVC>(_ primary   : PrimaryVC,
                                          _ secondary : SecondaryVC)
        where PrimaryVC: ViewController, SecondaryVC: ViewController
      {
        self.init(style: .doubleColumn, viewControllers: [
          AnyViewController(primary),
          AnyViewController(secondary)
        ])
        addChild(primary)
        addChild(secondary)
      }
      
      
      // MARK: - View
      
      public struct ContentView: View {
        
        @EnvironmentObject private var viewController : SplitViewController
        
        public init() {}
        
        struct EmbedChild: SwiftUI.View {
          
          let vc : _ViewController?
          
          var body: some View {
            if let vc = vc {
              vc.anyControlledContentView
            }
          }
        }
        
        public var body: some View {
          // SwiftUI switches the mode based on the _static_ style of the View
          switch viewController.style {
            case .doubleColumn:
              NavigationView {
                EmbedChild(vc: viewController.children.first)
                EmbedChild(vc: viewController.children.count > 1
                           ? viewController.children.dropFirst().first
                           : nil)
              }
            case .tripleColumn:
              NavigationView {
                EmbedChild(vc: viewController.children.first)
                EmbedChild(vc: viewController.children.count > 1
                           ? viewController.children.dropFirst().first
                           : nil)
                EmbedChild(vc: viewController.children.count > 2
                           ? viewController.children.dropFirst(2).first
                           : nil)
              }
          }
        }
      }
    }
    
    public extension AnyViewController {
    
      @inlinable // Note: not a protocol requirement, i.e. dynamic!
      var splitViewController : _SplitViewController? {
        viewController.splitViewController
      }
    }
    
    public extension _ViewController {
      
      /**
       * Return the ``SplitViewController`` presenting/wrapping this controller.
       */
      var splitViewController : _SplitViewController? {
        /// Is this VC itself being presented?
        if let presentingVC = presentingViewController {
          if let nvc = presentingVC as? _SplitViewController { return nvc }
          return presentingVC.splitViewController
        }
        if let parent = parent as? _SplitViewController {
          return parent
        }
        return parent?.splitViewController
      }
    }
    
    enhancement 
    opened by helje5 0
  • Replace `PushLink` with a `NavigationLink` initialiser

    Replace `PushLink` with a `NavigationLink` initialiser

    Would be nice if we could just keep using NavigationLink, instead of

    PushLink("Settings", to: SettingsPage())
    

    a

    NavigationLink("Settings", to: SettingsPage())
    

    The tricky part is capturing the navigation state. I think it can be solved, but needs a little more work.

    enhancement 
    opened by helje5 0
Releases(0.3.1)
  • 0.3.1(May 1, 2022)

    This minor release fixes a race when dismissing a sheet. When doing so, the ContentView would sometimes show an error while the sheet contents refresh during slide-out.

    Also allows setting the log level using the LOGLEVEL environment variable (error/debug are common values). Plus a few fixes in the debug panel.

    Finally the NavigationController now supports a style, so that navigation can be forced to use a stack on iOS (often appropriate in sheets on iPad).

    Source code(tar.gz)
    Source code(zip)
  • 0.3.0(Apr 30, 2022)

    This drops the "custom" presentation modifiers for sheets and navigations (presentAsSheet and presentInNavigation) as per issue #3. Most users are going to use the "auto presentation" (or PushLink) anyways.

    A user can still present w/ the .custom mode, but to perform the presentation, a respective .sheet or NavigationLink(isActive:) needs to be used (alongside the bindings offered by ViewController.

    Source code(tar.gz)
    Source code(zip)
  • 0.2.0(Apr 27, 2022)

    This release renames the contentView property to view, and more importantly: makes it a @ViewBuilder. This makes the simple flow even easier:

    class HomePage: ViewController {
    
      var view : some View {
        Text("Welcome Home!")
          .font(.title)
      }
    }
    

    Even less boilerplate and ViewControllerView is not really needed anymore.

    Source code(tar.gz)
    Source code(zip)
  • 0.1.0(Apr 27, 2022)

Owner
ZeeZide
ZeeZide GmbH ★ Software Consulting and Development
ZeeZide
An experimental navigation router for SwiftUI

SwiftUIRouter ?? An ⚠️ experimental ⚠️ navigation router for SwiftUI Usage ?? Check out ExampleApp for more. Define your routes: import SwiftUIRouter

Orkhan Alikhanov 16 Aug 16, 2022
An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

Composable Navigator An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind Vanilla S

Bahn-X 538 Dec 8, 2022
Helm - A graph-based SwiftUI router

Helm is a declarative, graph-based routing library for SwiftUI. It fully describ

Valentin Radu 99 Dec 5, 2022
Native, declarative routing for SwiftUI applications.

SwiftfulRouting ?? Native, declarative routing for SwiftUI applications Setup time: 1 minute Sample project: https://github.com/SwiftfulThinking/Swift

Nick Sarno 13 Dec 24, 2022
iOS architectures - MVC, MVP, MVVM, MVVM-C, ReactorKit, VIPER, Clean Architecture

iOS architectures - MVC, MVP, MVVM, MVVM-C, ReactorKit, VIPER, Clean Architecture, RIBs; Repository Pattern, Rxflow, Swinject, Tuist, Xcodegen, Cocoapods, SPM, Carthage + Rome

null 123 Dec 21, 2022
UIKit (MVC), Weather App

Weather Simple weather app written on UIKit (MVC) Was used • UITableView • CoreLocation • Lottie Animations • OpenWeatherMap (API) • Author of animate

null 5 Jun 29, 2022
A collection of iOS architectures - MVC, MVVM, MVVM+RxSwift, VIPER, RIBs and many others

ios-architecture WIP ?? ?? ?? ??️ Demystifying MVC, MVVM, VIPER, RIBs and many others A collection of simple one screen apps to showcase and discuss d

Pawel Krawiec 1.3k Jan 3, 2023
Easy quiz just for reviewing structures and MVC

Quizzler Our Goal The goal of this tutorial is to take you one step further in your journey of becoming an app developer. We are going to introduce yo

null 1 May 3, 2022
This project is a clone of YouTube. But the main intention is to show how to write clean code, using proper MVC patterns and re-usable coding methodologies!

YouTubeClone This project is a clone of YouTube. But the main intention is to show how to write clean code, using proper MVC patterns and re-usable co

Vamshi Krishna 169 Dec 10, 2022
Bill splitting and tip calculating app, build with UIKit and MVC pattern.

Split Check Bill splitting and tip calculating app, build with UIKit and MVC pattern. Features Enter bill total with number pad. Select tip % and betw

Anibal Ventura 0 Jan 7, 2022
Cryptocurrency price checker, build with UIKit and MVC + Delegate pattern.

Coin Check Cryptocurrency price checker. The app fetch from CoinAPI.io the latest coin prices, build with UIKit and MVC + Delegate pattern. Features S

Anibal Ventura 0 Jan 10, 2022
Quiz app using MVC.

Quizzler Our Goal The goal of this tutorial is to take you one step further in your journey of becoming an app developer. We are going to introduce yo

null 0 Jan 10, 2022
IOS-Quiz-App- - A trivia quiz app built with Swift using MVC structure

Quiz App A trivia quiz app built with Swift using MVC structure. Default Quiz

null 0 Feb 25, 2022
Quizzler - iOS App Design Pattern(MVC) and Code Structuring

Quizzler iOS App Design Pattern(MVC) and Code Structuring What you will learn Ho

null 0 Jan 10, 2022
PhotoApp - A Simple Photo App using Swift and MVC pattern

PhotoApp A Simple Photo App using Swift and MVC pattern After App launch, you wi

null 2 Aug 14, 2022
Ios-quizzer - The app implements basic features of a quiz app using MVC pattern

Quizzer App The app implements basic features of a quiz app using MVC pattern.

Stefan 2 May 10, 2022
A very simple To Do app to illustrate the principles from my A Better MVC talk

MVC Todo For more background on this repository, please consider reading these blog posts: https://davedelong.com/blog/tags/a-better-mvc/ What this sa

Dave DeLong 443 Jan 4, 2023
QuizApp - A simple Quiz App app using MVC

Quiz App Hey folks! I'm still learning Swift and I made a simple app again. This

Damla Çim 1 Jun 29, 2022
MMVMi: A Validation Model for MVC and MVVM Design Patterns in iOS Applications

MMVMi MMVMi: A Validation Model for MVC and MVVM Design Patterns in iOS Applications Motivation Design patterns have gained popularity as they provide

Mariam AlJamea 11 Aug 19, 2022