XUI is a toolbox for creating modular, reusable, testable app architectures with SwiftUI. With extensions to tackle common issues, XUI makes working with SwiftUI and Combine a lot easier!
- Easily keep your apps clean, maintainable and with a consistent app state
- Abstract view models with protocols
- Make more use of common SwiftUI and Combine components
- Find any object in deep hierarchies
In our blog articles
- "SwiftUI Architectures: Model-View, Redux & MVVM",
- "How to Use the Coordinator Pattern in SwiftUI" and
- "Handling Navigation in large SwiftUI projects",
we have already had a look at how to organize views and view models in SwiftUI. With all this knowledge, we have combined and summarized the most important and useful components in this library.
π₯ Features
- Abstraction of view models with protocols through the use of the
@Store
property wrapper - Deep Linking made easy β simply find any coordinator or view model in your app with a single call!
- Useful extensions to make the use of SwiftUI and Combine simpler!
πββοΈ Getting Started
Store
One of the integral parts of XUI is the Store
property wrapper. It makes it possible to define SwiftUI view models with protocols.
Let me guide you through the process: First, we create a protocol for our view model and make that conform to ViewModel
.
import XUI
protocol MyViewModel: ViewModel {
// You can specify properties and methods as you like
// This is just an example
var text: String { get set }
func open()
}
Secondly, we create an implementation for that protocol. Our implementation needs to be a class conforming to ObservableObject
and our protocol.
import XUI
class DefaultMyViewModel: MyViewModel, ObservableObject {
@Published var text: String
func open() {
// ...
}
}
Last but not least, we use the Store
property wrapper to use a protocol as view model in our view.
import XUI
struct MyView: View {
@Store var viewModel: MyViewModel
var body: some View {
TextField("Text", text: $viewModel.text)
}
}
As you can see, you can use your view model as you would with the @ObservedObject
property wrapper in SwiftUI. Instead of being constrained to a concrete type, you can specify a protocol instead. This way, we can write different implementations of the MyViewModel
protocol and use them in MyView
as well.
Deep Links
For deep links, we provide a search algorithm throughout your view model / coordinator hierarchy. You can use the DeepLinkable
protocol to provide access to your immediate children. To find a specific child in that hierarchy, you can use the firstReceiver
method on DeepLinkable
.
You can find a more extensive explanation in this blog article.
π€ΈββοΈ Extensions
XUI makes working with Combine and SwiftUI a lot easier!
Cancellable
When working with Combine extensively, there might be many occurences of .store(in: &cancellables)
in your code. To minimize code size and make code a bit more readable, we offer a function builder to insert multiple Cancellables
in a collection at once. Let's see it in action:
var cancellables = Set<AnyCancellable>()
cancellables.insert {
$myViewModel.title
.sink { print("MyViewModel title changed to", $0) }
$myViewModel.text
.sink { print("MyViewModel text changed to", $0) }
}
Publisher
With Publishers, you often work with singles or simply publishers that will only emit a single value or an error. To make working with these publishers easier (and since the Result
type is part of Swift now), we can simply build the following extensions:
var publisher: AnyPublisher<String, MyError>
publisher.asResult() // AnyPublisher<Result<String, MyError>, Never>
publisher.mapResult(success: { $0 }, failure: { _ in "Error occured." }) // AnyPublisher<String, Never>
publisher.tryMapResult(success: { $0 }, failure: { throw $0 }) // AnyPublisher<String, Error>
ViewModifiers
When using the Coordinator Pattern in SwiftUI (as discussed in this blog article), we need to inject a view modifier into a child view, so that transition logic is fully specified by the coordinator view rather than being distributed across views.
NavigationModifier
, PopoverModifier
and SheetModifier
are provided, with a similar interface to the actual modifiers.
View
To make working with NavigationView
simpler in SwiftUI, we provide a onNavigation
method that can be used, when you would like a closure to be performed, when a NavigationLink
is performed. Simply put it around your view, it will add a NavigationLink
itself.
Further, we add methods to your views for handling sheet
, popover
and navigation
with view model protocols.
Example:
struct MyView: View {
@Store var viewModel: MyViewModel
var body: some View {
NavigationView {
Text("Example")
.navigation(model: $viewModel.detailViewModel) { viewModel in
DetailView(viewModel: viewModel)
}
.sheet(model: $viewModel.sheetViewModel) { viewModel in
SheetView(viewModel: viewModel)
}
}
}
}
Binding
Working with bindings, especially when it concerns collections is hard - but no longer! We have written a few extensions to easily work with elements of collections using bindings.
var binding: Binding<[String]>
binding.first(equalTo: "example") // Binding<String?>
binding.first(where: { $0.count < 5 }) // Binding<String?>, this is not a practical example though
binding.first(equalTo: "example").forceUnwrap() // Binding<String>
binding.first(equalTo: "example").force(as: CustomStringConvertible.self) // Binding<CustomStringConvertible>
Further, one would possibly like to alter or observe the values being used through a binding.
var binding: Binding<String>
binding.willSet { print("will set", $0) }
// Binding<String>, will print whenever a new value is set by the binding, before it is forwarded to the initial binding
binding.didSet { print("did set", $0) }
// Binding<String>, will print whenever a new value is set by the binding, after it is forwarded to the initial binding
binding.ensure { !$0.isEmpty }
// Binding<String>, will only set the initial binding, when the condition is fulfilled
binding.assert { !$0.isEmpty }
// Binding<String>, will assert on get and set, that a condition is fulfilled
binding.map(get: { $0.first! }, set: { String($0) })
// Binding<String>, will map the binding's value to a different type
binding.alterGet { $0.prefix(1) }
// Binding<String>, will forward the altered value on get
binding.alterSet { $0.prefix(1) }
// Binding<String>, will forward the altered value on set to the underlying binding
π Example
As an example on how to use XUI in your application, we have written a Recipes App with the help of XUI.
π Installation
Swift Package Manager
See this WWDC presentation about more information how to use Swift packages in your app.
Specify https://github.com/quickbirdstudios/XUI.git
as the XUI
package link.
π¨βπ» Author
This framework is created with
π€ Contributing
Open an issue if you need help, if you found a bug, or if you want to discuss a feature request. Open a PR if you want to make changes to XUI.
π License
XUI is released under an MIT license. See License.md for more information.