Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/k-o-d-e-n/screenui
A multi-platform, multi-paradigm routing framework.
https://github.com/k-o-d-e-n/screenui
appkit composition deeplink ios multi-platform navigation routing screen-tree swiftui
Last synced: about 1 month ago
JSON representation
A multi-platform, multi-paradigm routing framework.
- Host: GitHub
- URL: https://github.com/k-o-d-e-n/screenui
- Owner: k-o-d-e-n
- License: mit
- Created: 2021-03-08T20:32:09.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2021-06-14T13:16:26.000Z (over 3 years ago)
- Last Synced: 2024-04-26T22:02:59.946Z (7 months ago)
- Topics: appkit, composition, deeplink, ios, multi-platform, navigation, routing, screen-tree, swiftui
- Language: Swift
- Homepage:
- Size: 96.7 KB
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 📲 ScreenUI
A multi-platform, multi-paradigm declarative routing framework for iOS/macOS and others, the replacement of Storyboard.
Supports `UIKit`, `AppKit`, `SwiftUI`.
### Real world example
```swift
static let transitionsMap = AppScreen(
Delegate(
ConditionalWindow {
Launch()
Welcome(
login: Present(Navigation(Login())),
registration: Present(Navigation(Registration()))
)
Tabs {
Navigation(
Feed(
page: Push(Page()),
match: Push(Match()),
gameDay: Push(GameDay(match: Push(Match()))),
tournament: Push(Tournament(match: Push(Match())))
)
)
Navigation(
Search(
filter: AnySearchFilter()
.navigation()
.configContent({ $0.isToolbarHidden = false })
.present(),
user: Present(Navigation(Player(team: Push(Team())))),
team: Team(player: Player().push())
.navigation()
.present(),
league: Present(Navigation(League())),
match: Present(Navigation(Match()))
)
)
Navigation(
Dashboard(
edit: Flow(base: UserEdit(editable: true)).present(),
entities: .init(
user: Push(Player(team: Team().navigation().present())),
team: Push(Team(player: Player().navigation().present())),
league: Push(League(
team: Push(Team()),
tournament: Push(Tournament(match: Push(Match())))
))
)
)
)
Navigation(
Messages(
settings: Present(
Settings(
account: Push(AccountInfo()),
changePassword: Push(ChangePassword())
).navigation()
)
)
)
}
.configContent({ tabbar in
tabbar.prepareViewAppearance()
})
.with(((), (), (), ()))
}
)
)
```## Table of contents
+ [**Main features**](#main-features)
+ [**Quick course**](#quick-course)
+ [**Deep dive**](#deep-dive)
+ [Screen](#screen)
+ [Transition](#transition)
+ [Screen path](#screen-path)
+ [Content builders](#content-builders)
+ [Cross-platform](#cross-platform)
+ [SwiftUI](#swiftui)
+ [UIKit](#uikit)
+ [AppKit](#appkit)
+ [**Best practices**](#best-practices)
+ [**Installation**](#installation)## Main features
+ Complete view of your app’s flow in one place
+ Isolating of screen transitions
+ Deep transitions (deep-link)
+ Abstracted implementation of the transition and its unification
+ Screen constants
+ Cross-platform core
+ Fully strong-typed codeWith `ScreenUI` you will forget about such methods, like `func pushViewController(_:)`, `func present(_:)`, about implementations are based on enums and reducers.
The best achievement of this framework is a combination of strictness (strong types, declarative style, transitions isolation) and flexibility (configurability of screens for each scenario, interchangeability of transitions, optional transitions).
## Quick course
Just like in Storyboard, the framework works in terms of screens.
```swift
struct SomeScreen: ContentScreen {
typealias NestedScreen = Self
typealias Context = Void
typealias Content = SomeView
/// define constants (color, title, and so on)
/// define transitionsfunc makeContent(_ context: Context, router: Router) -> ContentResult {
/// initialize a content view
/// pass `context` and `router` to content (constants will be available through `router`)
/// return the result
}
}
```Typical screen content implementation:
```swift
class SomeView {
let router: Router
let context: SomeScreen.Context
let title: Stringinit(router: Router, context: SomeScreen.Context) {
self.router = router
self.context = context
self.title = router[next: \.title]
}func didLoad() {
let textLabel = TextLabel(title: context.text)
...
}func closeScreen() {
router.back(completion: nil)
}
func moveNext() {
let nextScreenContext = ...
router.move(\.nextScreen, from: self, with: nextScreenContext, completion: nil)
}
}```
All you need in the next step is to build a [screen tree](#real-world-example) and show any screen from hierarchy:
```swift
transitionsMap.router[root: .default][case: \.0, ()].move(from: (), completion: nil)
```> Due to the specific interface of **SwiftUI** some things have small changes in API.
SwiftUI example
```swift
struct DetailView: View {
let router: Router
let context: Stringvar body: some View {
VStack {
Text(context)
/// optional transition
if let view = router.move(
\.nextScreen,
context: "Subdetail text!!1",
action: Text("Next"),
completion: nil
) {
view
}
/// move back
Button("Back") { router.back() }
}
.navigationTitle(router[next: \.title])
}
}
```## Deep dive
### Screen
Every screen must implement the protocol below:
```swift
public typealias ContentResult = (contentWrapper: S.Content, screenContent: S.NestedScreen.Content) where S: Screen
public protocol Screen: PathProvider where PathFrom == NestedScreen.PathFrom {
/// Routing target
associatedtype NestedScreen: ContentScreen where NestedScreen.NestedScreen == NestedScreen
/// *UIViewController* subclass in UIKit, *NSViewController* subclass in AppKit, *View* in SwiftUI, or your custom screen representer
associatedtype Content
/// Required data that is passed to content
associatedtype Context = Void
func makeContent(_ context: Context, router: Router) -> ContentResult
}
```Screens that are responsible for performing transitions must implement the protocol `ContentScreen`.
Screen containers (like *Navigation*) must implement the protocol `ScreenContainer` where `ScreenContainer.NestedScreen` is a transition target.
```swift
public struct Navigation: ScreenContainer where Root: Screen, Root.Content: UIViewController {
public typealias Context = Root.Context
public typealias Content = UINavigationController
public typealias NestedScreen = Root.NestedScreen
let _root: Rootpublic func makeContent(_ context: Root.Context, router: Router) -> ContentResult {
let (content1, content0) = _root.makeContent(context, router: router)
let content2 = UINavigationController(rootViewController: content1)
return (content2, content0)
}
}
```> [Read more]() about screens.
### Transition
Any transition must implement the protocol below:
```swift
public typealias TransitionResult = (state: TransitionState, screenContent: To.NestedScreen.Content) where From: Screen, To: Screen
public protocol Transition: PathProvider where PathFrom == To.PathFrom {
associatedtype From: Screen
associatedtype To: Screen
associatedtype Contextfunc move(from screen: From.Content, state: ScreenState, with context: Context, completion: (() -> Void)?) -> TransitionResult
}
```Transitions between the content screens must implement `ScreenTransition` protocol. Every such transition should provide a back behavior by assign `ScreenState.back` property.
```swift
public struct Present: ScreenTransition {
/// ...
public func move(from content: From.Content, state: ScreenState, with context: Too.Context, completion: (() -> Void)?) -> TransitionResult {
let nextState = TransitionState()
nextState.back = .some(Dismiss(animated: animated))
let (content1, content0) = to.makeContent(context, router: Router(from: to, state: nextState))
surface.present(content1, animated: animated, completion: completion)
return (nextState, (content1, content0))
}
}
```To make your screens more flexible, you can define type-erased transitions:
- `AnyScreenTransition` - supports transitions where `Context` is equal to *context* of the target content screen.
- `PreciseTransition` - supports transitions where `Context` is equal to *context* of the target container screen.So, when you will building [screen tree](#real-world-example), you can set up in one scenario one transition, another transition in the another scenario for the same screen.
> [Read more]() about transitions.
### Screen path
`Router` provides a subscript interface to build the path to the screen using *Swift Key-path expressions*:
```swift
/// [Initial screen] [Conditional screen] [Tab screen] [Some next screen in scenario] [Run chain from root screen content]
/// / / | / /
router[root: <%context%>][case: \.2, <%context%>][select: \.1][move: \.nextScreen, <%context%>].move(from: (), completion: nil)/// or using dot syntax
router.root(<%context%>).case(<%context%>).select(\.1).move(\.nextScreen, <%context%>).move(from: (), completion: nil)
```You can omit the context value if you sure that screen is presented in hierarchy.
### Content builders
Some screens can have dynamic content, for example `Tabs`. Therefore the framework provides `ScreenBuilder` protocol:
```swift
public protocol ScreenBuilder: PathProvider {
associatedtype Content
associatedtype Contextfunc makeContent(_ context: Context, router: Router) -> Content where From: Screen, From.PathFrom == PathFrom
}
```And of course for such instances is necessary Swift's result builder:
```swift
@resultBuilder
public struct ContentBuilder {}
```### Cross-platform
Framework API has cross-platform namespaces:
```swift
public enum Win {} /// Window implementations
public enum Nav {} /// Navigation implementations
public enum Tab {} /// Tabs implementations
extension Nav {
public enum Push { /// Push implementations
public enum Pop {} /// Pop implementations
}
}
public enum Presentation { /// Present implementations
public enum Dismiss {} /// Dismiss implementations
}
```For convenience, the framework provides protocols that enable typealiases to the nested types: `UIKitNamespace`, `AppKitNamespace`, `SwiftUINamespace`.
Apply one of them and you can write crossplatform code where:**Screens:**
- `Window` - a screen container that wraps a initial screen of your app.
- `Navigation` - a screen container that creates navigation stack.
- `Tabs` - a content screen that organize multiple screens to tab view interface.**Transitions**
- `Push` - a transition that pushes a new screen onto the navigation stack, with the corresponding `Pop` transition.
- `Present` - a transition that presents a new screen, covering the current screen, with the corresponding `Dismiss` transition.### SwiftUI
**Supported screens:**
- :white_check_mark: `Window`
- :white_check_mark: `Navigation`
- :white_check_mark: `Tabs`**Supported transitions:**
- :white_check_mark: `Push`/`Pop`
- :white_check_mark: `Present`/`Dismiss`### UIKit
**Supported screens:**
- :white_check_mark: `Window`
- :white_check_mark: `Navigation`
- :white_check_mark: `Tabs`**Supported transitions:**
- :white_check_mark: `Push`/`Pop`
- :white_check_mark: `Present`/`Dismiss`### AppKit
**Supported screens:**
- :white_check_mark: `Window`
- :white_check_mark: `Tabs`**Supported transitions:**
- :white_check_mark: `Present`/`Dismiss`## Best Practices
Screen appearance
You can define a protocol that will describe a screen appearance. So, you will create a single source of truth.
```swift
protocol ScreenAppearance {
var title: String { get }
var tabImage: Image? { get }
...
}
extension ScreenAppearance {
var tabImage: Image? { nil }
...
}
extension ScreenAppearance where Self: ContentScreen {
func applyAppearance(_ content: Content) {
/// configure content
}
}
protocol ScreenContent {
associatedtype Route: Screen
var router: Router { get }
}
extension ScreenContent where Route.PathFrom: ScreenAppearance {
func prepareAppearance() {
router[next: \.self].applyAppearance(self)
}
}
```
Universal transitions
There is screens that should available everywhere. So, you can extend `ContentScreen` protocol.
```swift
struct Alert: ContentScreen {
/// alert screen implementation
}
extension ContentScreen {
var alert: Present { get }
}/// now you can show alert from any screen
router.move(\.alert, from: self, with: "Hello world")
```
## Installation
```ruby
pod 'ScreenUI'
``````swift
.package(url: "https://github.com/k-o-d-e-n/ScreenUI", from: "1.1.0")
```## Author
Denis Koryttsev, @k-o-d-e-n, [email protected]
## License
ScreenUI is available under the MIT license. See the LICENSE file for more info.