Tags:
At the beginning of each year, I plan the books I want to read. This year, I decided to share my list publicly.
Few rules:
Since everything is clear now, let’s proceed with the list:
Polish Fantasy/Fantastic Channel (Kanał Fantastyczny) - a YouTube profile about fantasy and science fiction published a great video where Tomasz Kołodziejczak (writer and long-time head of the Polish comic book department in Egmont publishing house) gives his perspective on Isaac Asimov Foundation series reading order. Since I had already read the Foundation book and wanted to explore this world more, I found it very useful, but a video form wasn’t good to dive into and check what book I should read next. In this article, you will find a condensed list extracted from this video (the video is in Polish), which I recommend you watch first:
Foundation’s Triumph (Second Foundation Trilogy, #3)
SwiftUI contains a clever mechanism for adding toolbar actions. With one view modifier - toolbar
, we can control toolbar items in different places of the application. It doesn’t matter that you want to add a toolbar to the macOS navigation bar, iOS navigation bar, or iOS bottom bar; you will always start with .toolbar { }
.
SwiftUI 3 brings additional, longly awaited functionality - the possibility to displays toolbar actions above the system keyboard. You probably expect that already, but adding views to the keyboard toolbar is also very easy. Let’s take a look at a provided example.
To show this functionality, I created a small project with the TextEditor and three keyboard actions - two of them put ASCI emojis in TextEditor, and the third one hides the keyboard.
struct ContentView: View {
@State private var text = ""
@FocusState private var focus: Bool
var body: some View {
TextEditor(text: $text)
.focused($focus)
.frame(height: 150)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.primary, lineWidth: 1)
)
.padding()
}
}
Code above displays TextEditor
with some basic styling for better visibility. I also added the @FocusState
property wrapper, which helps programmatically displaying and hiding the keyboard. The @FocusState
is also a new addition in SwiftUI 3, but I’ll prepare a separate article about it.
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Shrug") { text += "\n¯\\_(ツ)_/¯" }
Button("Flip") { text += "\n(╯°□°)╯︵ ┻━┻" }
Spacer()
Button("Hide") { focus = false }
}
}
I told you it would be simple. In the content of the toolbar
view modifier, you have to put ToolbarItemGroup
view with placement parameter set to keyboard. You can place any View in the content ViewBuilder of ToolbarItemGroup
. Usually, you will want to use Buttons
and Spacers
to align them properly.
The first two buttons modify text property by adding correct ASCI characters; the third sets focus property to false. Calling this will hide the keyboard if it’s visible.
Below you can find the complete code of this experiment and a link to the Xcode project.
import SwiftUI
struct ContentView: View {
@State private var text = ""
@FocusState private var focus: Bool
var body: some View {
TextEditor(text: $text)
.focused($focus)
.frame(height: 150)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.primary, lineWidth: 1)
)
.padding()
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Shrug") { text += "\n¯\\_(ツ)_/¯" }
Button("Flip") { text += "\n(╯°□°)╯︵ ┻━┻" }
Spacer()
Button("Hide") { focus = false }
}
}
}
}
Animations are a crucial but often neglected part of good UI. Correctly used can guide used in the app. In this article, you will learn how to make a list cell that expands on tap and apply animation to it.
We will create a simple Todo List with subtasks.
First of all, let’s make a models for this data.
struct Task: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
Of course, it won’t be a fully working example. In the final application, you will probably need additional fields for selected state, deadline, etc.
Then let’s focus on cells. I decided to go with three separate cells designs. One for Subtask
model:
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
one when a new/empty subtask:
struct EmptySubtaskCell: View {
@State private var text: String = ""
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
TextField("new task", text: $text)
}
}
}
and one for a Task
model:
struct TaskCell: View {
@State private var isExpanded: Bool = false
let task: Task
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
ForEach(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
EmptySubtaskCell()
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation { isExpanded.toggle() }
}
}
}
That last one will support expanding and collapsing, so it’s a bit bigger. Let’s make a small dissection.
In the body, we load content and ensure that the cell takes the entire width of a given view.
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
The TaskCell
will store information about its state. You can find the isExpanded
@State
property wrapper on the top of it.
@State private var isExpanded: Bool = false
The cell’s content displays a header where the title and checkbox are displayed and subtasks if the cell is expanded. At the bottom, it also shows the Divider
to separate one task from another.
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
ForEach(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
EmptySubtaskCell()
}
.padding(.leading)
}
Divider()
}
}
The cell’s header is similar to the SubtaskCell
, but it displays a square image instead of a circle. It also has the onTapGesture
method, which toggles the isExpanded
state.
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture { isExpanded.toggle() }
}
With all these cells, you are ready to create a final view.
struct TasksView: View {
private let tasks: [Task] = [
Task(title: "Create playground", subtask: []),
Task(title: "Write article", subtask: []),
Task(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots")
]
),
Task(title: "Publish article", subtask: [])
]
var body: some View {
NavigationView {
ScrollView {
ForEach(tasks) { task in
TaskCell(task: task)
.animation(.default)
}
.navigationTitle("Todo List")
}
}
}
}
At the top are some static Tasks
hardcoded (for the production-ready application, you should store them in application state.)
Body list all tasks using the ForEach
view. And how I achieved this animation effect, you may ask. It was as simple as adding the .animation(.default)
view modifier to the TaskCell
view.
By following this tutorial, you will achieve an effect presented at the top of the article. With the help of SwiftUI, adding animations that delights the user experience can be straightforward.
You can find the full code of this project below:
struct Task: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct TasksView: View {
private let tasks: [Task] = [
Task(title: "Create playground", subtask: []),
Task(title: "Write article", subtask: []),
Task(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots")
]
),
Task(title: "Publish article", subtask: [])
]
var body: some View {
NavigationView {
ScrollView {
ForEach(tasks) { task in
TaskCell(task: task)
.animation(.default)
}
.navigationTitle("Todo List")
}
}
}
}
struct TaskCell: View {
@State private var isExpanded: Bool = false
let task: Task
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
ForEach(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
EmptySubtaskCell()
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture { isExpanded.toggle() }
}
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct EmptySubtaskCell: View {
@State private var text: String = ""
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
TextField("new task", text: $text)
}
}
}
Quality by Code Climate is a web service that gives you analytics for your code. It analyzes your code for code smells, and with proper CI/CD integration, it can track changes in test coverage data.
This article will list, step by step, how to set up Code Climate test coverage upload from Bitrise - probably most famous and widely used mobile Continues Integration and Continues Delivery service.
Initial assumption: Your GitHub repository is added to the Quality by Code Climate and the Bitrise.
Get Code Climate’s test reporters ID - visit Repo Settings
on Code Climate and open Test coverage
.
Add ID from the previous step as a Bitrise Secret with a name CC_TEST_REPORTER_ID
(if you want to test Pull Requests active Expose for Pull Requests?
)
Generate GitHub personal access token (only public_repo
permission is needed)
Add token from the previous step as a Bitrise Secret with a name GITHUB_ACCESS_TOKEN
(if you want to test Pull Requests active Expose for Pull Requests?
)
Turn on gathering test coverage in your Xcode project. Edit Scheme...
-> Test
phase and select checkbox on Gather coverage for...
in Code Coverage
section.
In Bitrise, go to your Tests workflow, and before Xcode Test for iOS
step, add a new Script phase with content visible below. It will download Code Climate test runner (cc-test-reporter
) and updates Code Climate that test coverage is being prepared.
#!/usr/bin/env bash
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-darwin-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
./cc-test-reporter before-build
Xcode Test for iOS
and paste provided content. This script uses xcrun xccov
to format Tests.xcresult
file to JSON and uploads it using the previously downloaded cc-test-reporter
script.#!/usr/bin/env bash
xcrun xccov view --report $BITRISE_XCRESULT_PATH --json > coverage.json
./cc-test-reporter after-build --coverage-input-type xccov
Generate code coverage files
? to no
. Note: I’m not sure why, but I had a problem finishing that step with this set to yes
.That’s it. With that few simple steps, you can send test coverage data from Bitrise to Code Climate.
24.7.2021 16:07How to setup Code Climate Quality test coverage with Bitrise
If you are using defaults colors for text and backgrounds, iOS/macOS handles dark mode literary by itself. But sometimes, you want to go with your style to make a brand more appealing or differentiate from the competition. Adjusting to dark mode required more work then, but still, the framework offers you many different options to make your life easier.
This article will explain how to use the ColorScheme
environment variable to differentiate between light and dark mode and show overlay over the image when it is visible in the dark mode.
Let’s say that you are working on the sign-in screen for your application. You decided to use an image as a background to make it more appealing for users. But then you realize that you pick a photo with too vibrant colors for dark mode. There are few solutions for that.
Xcode project assets can support different assets for different modes. So you can edit the current image or find a different one that the application will use for a dark mode. But let’s say that you want to add new (usually big) assets to the project. What is the other option? You can use the ColorScheme
environment variable to check the current mode and display a dark overlay above the image.
SwiftUI offers an Environment property wrapper to get environment variables. One of them, available by default under the colorScheme
key, is a ColorScheme
enum which can store two values, light
and dark
. To get that setting, add:
@Environment(\.colorScheme) private var colorScheme: ColorScheme
to your view.
Like nearly everything in SwiftUI, you can achieve a dim effect in many ways. I decided to go with ZStack
and to display a rectangle with a translucent color.
if colorScheme == .dark {
Rectangle()
.fill(Color.black.opacity(0.3))
}
You can check the complete code example:
struct BackgroundView: View {
@Environment(\.colorScheme) private var colorScheme: ColorScheme
var body: some View {
ZStack {
Image("background")
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
if colorScheme == .dark {
Rectangle()
.fill(Color.black.opacity(0.3))
}
Text("Swift Wombat")
.font(.largeTitle)
.bold()
}
.ignoresSafeArea()
}
}
The gallery below presents this effect in light and dark mode. As you can notice, the Swift Wombat title color was adjusted automatically.
Pull to Refresh is a widespread UX pattern on mobile. Drag a list down and load new items. You can find that in all social media applications and almost all other apps that display data download from servers.
To achieve this in previous versions of SwiftUI, you will have to use UIKit
code, create your view, or third-party library. But SWiftUI 3 brings a native ViewModifier
that changes adding Pull to Refresh into child’s play. Let’s dive into code.
To display Pull to Refresh, you have to add a refreshable
modifier to the scrollable view, most commonly List
or Grid
. The refreshable
view modifier uses async/await syntax and handles dismissing the refresh indicator automatically.
struct ContentView: View {
@State private var rows = 0
var body: some View {
List(0..<rows, id: \.self) { number in
Text("row \(number)")
}
.refreshable {
await Task.sleep(5_000_000_000) // wait 5s
rows += 10
}
}
}
For this example, I’m using a Task.sleep
that simply waits for a given number of nanoseconds to pass. Typically, you will perform time-consuming asynchronous (like downloading data from the network) operations here.
And that’s it. This code sample will add full Pull to Refresh handling. But there is one more thing that is worth mentioning. SwiftUI 3 added a new modifier to download initial data for a given view. This modifier is called task
and also expects the async/await syntax.
struct ContentView: View {
@State private var rows = 0
var body: some View {
List(0..<rows, id: \.self) { number in
Text("row \(number)")
}
.task {
await Task.sleep(3_000_000_000) // wait 3s
rows = 10
}
.refreshable {
await Task.sleep(5_000_000_000) // wait 5s
rows += 10
}
}
}
Each WWDC brings lots of new goods for all Apple ecosystem developers. The 2021 edition wasn’t different. AsyncImage view is a new SwiftUI addition that expects a URL instead of an asset to display images. Read further to learn how to use it.
Displaying images from the web is a widespread task in mobile applications. You can see this in all social media applications, web-based knowledge bases, or apps that stream files from servers. It requires more work than displaying images locally. After all, we have to wait until the image is loaded (usually displaying a placeholder) and handle situations where the network connection is not reliable (or the file doesn’t exist anymore). AsyncView
is created to make those operations easier. When it comes to displaying images, it uses an Image
view underneath.
The simplest form of AsyncView requires only the url
parameter with the URL object. This url
parameter is optional, so you don’t have to check its existence when creating it using the URL(string:_)
constructor.
let url = URL(string: "http://placekitten.com/200/200")
AsyncImage(url: url)
But often, you may want to do more with a result. One of the most common scenarios was to resize the image to fit its frame. The AsyncImage
first initializer can handle scale
parameter and two convenient closures - content
and placeholder
. The first one performs operations on the Image
view when it is finally loaded, and the second one returns a view displayed when an image is loading.
AsyncImage(url: url) { image in
image.resizable()
} placeholder: {
Text("Loading...")
}
With content closure, you can resize the image, fit it to content or apply rounded corners.
But there is a second initializer that allows for more advanced control. You will probably be using that version more often. It also supports url
and scale
properties, but there is a new transaction
parameter and content closure gets AsyncImagePhase enum. You can use the data provided in that enum to return a view for different phases of image loading. When the network file is loaded (success
phase), you will get an Image
view. For failure
error is returned. There is a third empty phase, which means that the file is being loaded. The transaction
parameter is used to provide an animation that is applied when phases change.
AsyncImage(
url: url,
transaction: Transaction(
animation: .easeInOut
)
) { phase in
if let image = phase.image {
image.resizable()
} else if phase.error != nil {
Text("Error!")
} else {
Text("Loading...")
}
}
Button
, as one of the most common UI elements, is used for versatile sets of actions. But actions can have a different level of importance. Sometimes you want to highlight the primary activity or mark the destructive one in red. You can do this by wrapping the Button view in a custom view, but SwiftUI creators design a better way by using buttonStyle(_:)
view modifier.
The buttonStyle
modifier accepts one argument, which is an object that implements the PrimitiveButtonStyle
protocol. There are some predefined structs that you can use out of the box - DefaultButtonStyle
, PlainButtonStyle
, and BorderlessButtonStyle
. To use them, call:
Button("Default button style", action: {})
.buttonStyle(DefaultButtonStyle())
Button("Plain button style", action: {})
.buttonStyle(PlainButtonStyle())
Button("Borderless button style", action: {})
.buttonStyle(BorderlessButtonStyle())
You can look at the bottom of the article to find out how they look on iOS. But that styles adjust to the platform. They will look different on iOS, watchOS, and macOS.
Those are still minimal options. But you don’t have to stop here. Create your struct, implement PrimitiveButtonStyle
protocol, which requires only one makeBody(configuration: Configuration) -> some View
method, and you can create as many styles as you want.
The makeBody function is responsible for creating a button, so usually it w contains something like this Button(configuration)
, but you can modify the configuration or create a new one before you pass it to the Button view initializer.
In the example below, I created a button with a rounded border. Background color can be controller by the color
property:
struct BorderedButtonStyle: PrimitiveButtonStyle {
let color: Color
func makeBody(configuration: Configuration) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(color)
Button(configuration)
.foregroundColor(.white)
.padding()
}
}
}
To apply the new style to the Button, use the same modifier as above:
Button("Bordered button style", action: {})
.buttonStyle(BorderedButtonStyle(color: .orange))
Custom button styles can be a straightforward solution to prepare a tailored set of button styles dedicated to your app.
Foundation framework available for iOS, macOS, iPadOS, and other Apple platforms hides many gems that you can use to simplify operations required in nearly every application. One of the very often used sets of features are formatters. You probably are familiar with DateFormatters
, but in today’s article, I’ll describe how to use PersonNameComponentsFormatter
, which you can use to parse and format a person’s name.
The structure of personal names varies between different locales. On the other hand, there are significant and meaningful for persons that use them. That’s a reason you should treat them with respect and very carefully. PersonNameComponentsFormatter
can help you with that. This formatter can parse string data provided and separated them into PersonNameComponents
, and get that structure to display name using the correct locale.
First, you have to create an instance of the PersonNameComponentsFormatter
:
let formatter = PersonNameComponentsFormatter()
To separate name string for components use personNameComponents(from:)
function:
if let components = formatter.personNameComponents(
from: "Sir David Frederick Attenborough"
) {
print(components)
/* Prints:
namePrefix: Sir
givenName: David
middleName: Frederick
familyName: Attenborough
*/
}
If you already have components (for example, created from the form data), you can use them with the same class to build different data representations. You can use one of 4 styles short
, medium(default)
, long
, abbreviated
.
formatter.style = .default
print(formatter.string(from: components))
// David Attenborough
formatter.style = .abbreviated
print(formatter.string(from: components))
// DA
formatter.style = .long
print(formatter.string(from: components))
// Sir David Frederick Attenborough
formatter.style = .medium
print(formatter.string(from: components))
// David Attenborough
formatter.style = .short
print(formatter.string(from: components))
// David
Additionally, PersonNameComponentsFormatter
can prepare an Attributed String.
As you can see, PersonNameComponentsFormatter
is very easy to use. With correctly collected or parsed data, you will create correct abbreviations or long-style formatted person details.
When you are scaling up (and down) a small image in SwiftUI, there is an operation called interpolation. The size of a current image pixel is calculated based on neighbor pixels. It gave a more natural and better-looking effect, but it also means that the image appears blurry (look at the first image in the example above). More advanced interpolations also increase computational complexity.
Sometimes, you want to display the image in a more pixelated form (second image in the example above). There is a view modifier for that. Its name is an interpolation(...)
(surprise, surprise).
Top: Interpolation - default, Bottom: Interpolation - none
Using this modifier is very easy. It expects one of four Interpolation
enum values: high
, medium
, low
, and none
. The first three will control interpolation quality, and the last one goes with the most straightforward approach of duplicating current pixel values (and gives pixelated effect).
To change interpolation quality, call the given view modifier on Image view:
Image("wombat")
.interpolation(.none)
You will probably not need that function too often, but it’s good to know its existence and play with that setting a bit.
If you are an iOS or macOS user, you have seen alerts many times. Alerts are components that inform you about some errors or ask about some destructive actions. It would be best if you were careful not to overuse them in your app because it may distract users.
SwiftUI offers a structure that makes displaying alerts very easy, but you must remember two limitations:
Due to the nature of SwiftUI, you must specify your alerts as an element of the view’s tree. You will expect that displaying an alert should be a function, but it will be imperative programming. In the declarative paradigm, you specify the whole interface before the app starts. Messages displayed to users must also conform to that rule.
You will add alerts as view modifiers. If you add two of them at the same level, the second one will override the first. In that case, it’s better to create an enum that will combine all alerts that you want to present or, as you will see in the example below, add alerts somewhere lower the view’s tree.
With that two limitations in mind, you can go to the fun part - implementation. Alerts are added to the view’s tree at the beginning, so they need some Binding
, usually a @State
that will tell they should be displayed or not.
There are two versions of the alert(...)
view modifier. The first one expects a boolean value that represents isPresented
state of alert. You modify your state, setting it true to display it.
struct SimpleAlert: View {
@State private var showAlert = false
var body: some View {
Button("Show alert") {
showAlert = true
}
.alert(isPresented: $showAlert) {
// Alert view
}
}
}
One thing is worth noticing. You need to use $
before the name of your @State
variable to get Binding<Bool>
from it.
The content block of the alert(..)
modifier expects an Alert
view. Its initializers look like this:
public init(
title: Text,
message: Text? = nil,
dismissButton: Alert.Button? = nil
)
public init(
title: Text,
message: Text? = nil,
primaryButton: Alert.Button,
secondaryButton: Alert.Button
)
You can use the first one for simple confirmation messages (like error or info messages). Only title Text view is required for this one. By default, it displays an OK button.
The second one gives you more freedom. You must set a primaryButton and a secondaryButton for it. You can select from cancel(...)
, default(...)
and destructive(...)
button styles. System calls primary button when the view is dismissed, so watch out to not set any destructive action there.
Alert(
title: Text("Uninstall this application"),
message: Text("Do you want to uninstall this application? This action cannot be undone."),
primaryButton: .cancel(),
secondaryButton: .destructive(
Text("Uninstall"),
action: { /* do something */ }
)
)
You don’t have to worry about setting showAlert
to false
again. SwiftUI will handle this for you. The cancel button is empty in this example.
Full code to display simple alert look like this:
struct SimpleAlert: View {
@State private var showAlert = false
var body: some View {
Button("Show alert") {
showAlert = true
}
.alert(isPresented: $showAlert) {
Alert(
title: Text("Uninstall this application"),
message: Text("Do you want to uninstall this application? This action cannot be undone."),
primaryButton: .cancel(),
secondaryButton: .destructive(
Text("Uninstall"),
action: { /* do something */ }
)
)
}
}
}
The second version of alert(...)
can be used when the Alert state depends on the property bound to it. Instead of isPresented
, you provide binding to an optional item
- a struct, class, or enum that conforms to Identifiable
protocol.
enum Animal: String, CaseIterable, Identifiable {
case wombat, koala, platypus
var id: String { rawValue }
}
struct AnimalPicker: View {
@State private var selectedAnimal: Animal?
var body: some View {
VStack {
Text("Please select an animal:")
button(for: .wombat)
button(for: .koala)
button(for: .platypus)
}
.alert(item: $selectedAnimal) { animal in
Alert(
title: Text("Selected animal:"),
message: Text(animal.rawValue.capitalized)
)
}
}
private func button(for animal: Animal) -> some View {
Button(animal.rawValue.capitalized) {
selectedAnimal = animal
}
}
}
Content block of that version of the view modifier the same as the previous one expects Alert view, but it provides you a selected item. When the selected item is nil alert is not displayed. But when you set it to an existing object, the Alert
view will show. When the message is dismissed, the selected item is set back to nil
.
SwiftUI offers an easy way to inject your model classes into a view hierarchy. Additionally, it handles model changes pretty automatically, and only a tiny amount of additional code is needed from your side. In this article, I’ll explain how to provide and retrieve @EnvironmentObject
anywhere within your views hierarchy.
Why do you need an environment object, you may ask? It simplifies your code. Of course, you can create a @State or @StateObject in your top view and provide it as Binding down below, but this will make your code less modular. Think of environment objects like a dependency injection mechanism. You provide your model in one place, and then in another, you can read it or perform actions. My favorite Redux-like state management library SwiftDux uses environment objects to provide store and action dispatcher to any view down the graph.
The first puzzle you need is a class (model) that you will pass as an environment object. It needs to conform to the ObservableObject
protocol. In that class, you can use the @Published
property wrapper to expose a variable. When you change the value of the @Published
variable, it will inform ObservableObject
, and SwiftUI will handle the rest. Of course, a given object can contain standard variables and methods as well.
class Counter: ObservableObject {
@Published var value = 0
}
Now, when you have your model ready, you must find a place to create an instance of it and inject it into the SwiftUI view structure. Usually, this is the first view of the view’s hierarchy. But it doesn’t have to be. Sometimes, you want to provide some data to just a part of a structure. When you pick your place, create an instance of your model class using the @StateObject
property wrapper, and then inject it using the environmentObject
view modifier
struct ContentView: View {
@StateObject private var counter = Counter()
var body: some View {
CounterView()
.environmentObject(counter)
}
}
You created your model, provided it for SwiftUI views, so it’s time to learn how to use it. To retrieve your model, you should use the @EnvironmentObject
property wrapper. You can operate on the provided object, and SwiftUI will refresh it in other places of the UI where the given environment object is used.
struct CounterView: View {
@EnvironmentObject private var counter: Counter
var body: some View {
VStack {
Text("\(counter.value)")
Divider()
IncrementView()
}
}
}
struct IncrementView: View {
@EnvironmentObject private var counter: Counter
var body: some View {
Button("Increment", action: { counter.value += 1 })
}
}
Command + I is a very commonly used Xcode shortcut. It is used to fix the indentation of the selected code part. But it does no more than that. What’s worst, it adds four spaces (or one tab if you use it for indentation) to empty lines. This article will learn how to install, configure and use a third-party command-line application called SwiftFormat for better Swift source code formatting.
SwiftFormat is an open-source project created and maintained by Nick Lockwood - creator of a few famous and commonly used iOS libraries. It formats your code using more than 50 rules, which you can turn off and turn on individually. If you follow just the basic code formatting principles, you can be sure that SwiftFormat will handle the rest, giving you well-formatted code.
You can install SwiftFormat using one of the provided methods. I’ll describe only one that is easiest for me - by using Homebrew.
You have to first install Homebrew and then call
brew install swiftformat
And that’s it. SwiftFormat is installed and ready to use.
To run SwiftFormat over your project, go into the console to the project catalog and call:
swiftformat .
The presented command will format all .swift
files into this and all subdirectories. For the first run, I advise you to commit or stash your current code changes.
If you go to documentation, you will find that you can control different properties. You can specify them using the command line argument of the .swiftformat
file placed in the same director.
I like default settings, so the only thing I do is turn off formatting on some generated directories (like CoreData or Pods) and directly specify the Swift version. I made the swiftformat.sh
file for this that looks similar to this one:
#!/bin/bash
# Install swiftformat via brew first with `brew install swiftformat`
swiftformat . --swiftversion 5.3 --exclude Shared/Resources,Shared/Models/CoreData --disable wrapMultilineStatementBraces
Swift (SwiftUI in particular) commonly uses property wrappers to make repeatable operations easier. For example, the @State is used for observing and storing view state, and @AppStorage to store app parameters using local persistence.
Learn how to use AppStorage to access UserDefaults in SwiftUI.
But you are not limited to build-in property wrappers. You can make your property wrappers and add additional functionalities to struct and class properties (property wrappers are not supported at the top level).
This article will learn how to build a super simple property wrapper that will print the current value when you read or write it. It can help debug, but the goal is to show you how custom property wrappers work.
Property wrapper is a struct marked with @propertyWrapper
attribute. It requires a wrappedValue
property.
@propertyWrapper
struct CustomWrapper<Value> {
var wrappedValue: Value
}
That’s it. You can now use your property wrapper on any property.
@CustomWrapper var value = 0
But in the current state, it doesn’t do anything. Let’s add some logic when the wrapped value is set or get.
@propertyWrapper
struct Logged<Value> {
private var value: Value
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
print("Value get: \(value)")
return value
}
set {
print("Value set: \(newValue)")
value = newValue
}
}
}
When you try to read or change a given property, you will also see information about a current state on the console.
// Property wrappers are not supported on top level
struct Object {
@Logged var value = 0
}
var object = Object()
print("Value is equal to: \(object.value)")
print("---")
object.value = 100
print("---")
print("Value is equal to: \(object.value)")
/*
Prints:
Value get: 0
Value is equal to: 0
---
Value set: 100
---
Value get: 100
Value is equal to: 100
*/
Of course, the provided example is elementary, but property wrappers can be used in various cases. Some libraries help with data validation and simplify usage of codable.
In your SwiftUI applications, you will often use the Button view. This interactive control is used to build simple user inputs. But in this article, you will learn how to disable buttons.
Think about a situation as follow - you have three different options, but some of them are only active when the toggle is true. You can hide inactive options using the if
statement, but this will be counterintuitive for users. The “Don’t make me think” principle assumes that users should know what’s going on and not be surprised by UI. You can achieve it by just disabling that options.
To disable
a button, you should use the disabled view modifier. It takes a bool parameter, which, for example, can be taken from the state.
@State private var disabled = true
var body: some View {
Button("Press me", action: {})
.disabled(disabled)
}
You can use the disabled
modifier on a view above buttons (VStack
, for example). That way, you will apply it to all buttons (or other controls below).
VStack {
Button("Press me 1", action: {})
Button("Press me 2", action: {})
Button("Press me 3", action: {})
}
.disabled(true)
Note: According to the documentation, disabled
from a view higher in scope takes precedence over disabled from lower views. But my experiments (you can find a link to the project below) show that priority has one that its parameter is set to true
and order is not important.
List
view is a container that can present a scrollable list of views in SwiftUI. You can adjust its look, and it supports versatile actions and change its style depending on the platform. This article will learn how to use the List to display navigation links, data from an array and use ForEach and Section to display multi-dimensional data structures.
As always, SwiftUI creators made it easy to use API. You can display a basic List
with only a few lines of code.
List {
Text("Identifiable List")
Text("Multi-Dimensional List")
Text("Inset Grouped List Style")
}
Additionally, wrapping your items in List instead of VStack
will make them scrollable. So, if there are more items that fit on the screen, they will still be accessible to users.
You can use the List view to build the app navigation. When you wrap your Texts
(or any other View) in NavigationLink
, they will get an additional details arrow and, together with NavigationView
, will handle switching between screens.
NavigationView {
List {
NavigationLink(destination: IdentifiableList()) {
Text("Identifiable List")
}
NavigationLink(
destination: MultiDimensionalList()
.navigationTitle("Multi-Dimensional List")
) {
Text("Multi-Dimensional List")
}
NavigationLink(
destination: MultiDimensionalList()
.listStyle(InsetGroupedListStyle())
.navigationTitle("Inset Grouped Style")
) {
Text("Inset Grouped List Style")
}
}
.navigationTitle("Simple list")
}
When using lists, you usually have an array of data. One of the List
initializers can get that data and generates a list of items. It will handle rendering only visible entities at a given moment which is essential for application memory usage.
The data that you provide must conform to the Identifiable protocol. It’s straightforward to achieve. Your struct must have an id property that conforms to Hashable
. String
or UUID
type is usually perfect for this.
struct Continent: Identifiable {
let id: UUID = UUID()
let name: String
}
private let continents = [
Continent(name: "Asia"),
Continent(name: "Africa"),
Continent(name: "North America"),
Continent(name: "South America"),
Continent(name: "Antarctica"),
Continent(name: "Europe"),
Continent(name: "Australia")
]
With struct declared like the one above, you can easily display an array of structs with just three lines:
List(continents) {
Text($0.name)
}
With the support of two other views - ForEach
and Section
you can create a stunning two-dimensional list. For this example, I added an array of animals to a Continent
struct.
struct Continent: Identifiable {
let id: UUID = UUID()
let name: String
let animals: [Animal]
}
struct Animal: Identifiable {
let id: UUID = UUID()
let name: String
}
When you build your example structure, what is only need to iterate over items. The Section
view displays a header, and objects in each behave like normal list cells.
List {
ForEach(continents) { continent in
Section(header: Text(continent.name)) {
ForEach(continent.animals) { animal in
Text(animal.name)
}
}
}
}
This article covers just an iceberg of list features, but I want to mention one more element that, with just one line, will take your design to the next level. I’m talking about the listStyle
modifier. With one line:
.listStyle(InsetGroupedListStyle())
You may change a list from the previous section to the one visible at the start of this one.
One of the biggest arguments in favor of using SwiftUI in production apps is the interoperability of UIKit. If you have a problem with achieving something with pure SwiftUI, you can always use UIViewRepresentable
to wrap UIKit view and put it in the SwiftUI app only in dozen lines of code. How to do this? Read further.
This article will teach you how to wrap UITextView
into UIViewRepresentable
protocol, with the Coordinator
notifying your app when the text changes.
I used an elementary example with UITextView
, but creating UIViewRepresentable will always look very similar. First, you have to wrap your view in the protocol and then build a Coordinator
to handle UIKit delegate and data source methods.
UIViewRepresentable protocol requires confirmation for two methods.
associatedtype UIViewType: UIView
func makeUIView(context: Self.Context) -> Self.UIViewType
func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)
The makeUIView
method should create a UIKit view and returns it for SwiftUI to handle. SwiftUI calls this method when the view is created. Then updateUIView
is called when your view should be updated. Both methods bring you a UIViewRepresentableContext
struct which you can use to get Coordinator
or various environment values.
A simple implementation for UITextView
may look like this:
struct TextView: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.systemGray.cgColor
view.layer.cornerRadius = 8
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
When used in SwiftUI, the code above will display a UITextView
with a border for better visibility.
But you may want to provide the initial text value and refresh text if it changes from outside. For this, you should use a Binding
property wrapper and updateUIView
.
struct TextView: UIViewRepresentable {
@Binding var text: String // NEW
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.systemGray.cgColor
view.layer.cornerRadius = 8
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text // NEW
}
}
You can use this view in SwiftUI as follow:
struct ContentView: View {
@State private var text: String = "Initial text"
var body: some View {
TextView(text: $text)
}
}
What you saw is just half of the equation. You will also need to know when UITextView
content did changed. For this, UIViewRepresentable
offers an optional method:
associatedtype Coordinator = Void
func makeCoordinator() -> Self.Coordinator
that you can implement and along with Binding property used to inform SwiftUI about current text UITextView
.
First, start with the makeCoordinator
method. This method can return any entity, no matter struct or class.
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
Your Coordinator
object will receive the text
variable as a Binding (returned with $
syntactic sugar).
Ok, now you can go to Coordinator
class implementation.
class Coordinator: NSObject, UITextViewDelegate {
@Binding private var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
text = textView.text
}
}
As you can see, it implements UITextViewDelegate
protocol (and NSObject
required by it), and in textViewDidChange
, it updates the text variable. In this example, the Coordinator
is a class, so it needs an init
method that sets Binding<String>
to local property wrapper using _text
syntax.
There is one puzzle missing. You have to connect your Coordinator
with UITextView
. Do this in makeUIView
, but don’t create it there. Use the one provided in context.
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator // NEW
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.systemGray.cgColor
view.layer.cornerRadius = 8
return view
}
As you can see, using UIKit views in SwiftUI is easy. You can check the complete code, along with an example of how to use it with the State property wrapper below.
struct ContentView: View {
@State private var text: String = "Initial text"
var body: some View {
TextView(text: $text)
}
}
struct TextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.systemGray.cgColor
view.layer.cornerRadius = 8
return view
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator: NSObject, UITextViewDelegate {
@Binding private var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
text = textView.text
}
}
}
Rounded corners, like many other things in SwiftUI, can be added in few different ways. In this article, you will learn the simples one and one more sophisticated that you may find helpful in some circumstances. Let’s start.
The easiest way to add rounded corners to any view is by using the cornerRadius modifier. Of course, you have to add background to see the effect of this modifier. You can achieve the example above with these five lines of code:
Text("Swift Wombat")
.font(.largeTitle)
.padding()
.background(Color.green)
.cornerRadius(16)
The cornerRadius
modifier supports two parameters. The first unnamed one is radius
, and the second one is antialiased
, which is by default set to true.
Instead of using cornerRadius, you can create a more sophisticated view by using ZStack.
I used a similar solution to create chat bubbles.
A solution like this isn’t probably recommended to add only a background with rounded corners, but you may find it useful to start playing with advanced views. This solution begins with the RoundedRectangle
shape. Since the Shape
struct is also a View, you can use it as a base for your creations. Putting RoundedRectangle
into ZStack
, just above your given view, will create a rounded background effect.
ZStack {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(.green)
Text("Swift Wombat")
.font(.largeTitle)
.padding()
}
When creating the RoundedRectangle
struct, you can provide a cornerRadius
parameter.
There is also a third solution, often used to create borders. This solution uses the overlay
to display a view above another view. If you use it for the rounded background, it will cover your creation. But it will work perfectly for borders.
Text("Swift Wombat")
.font(.largeTitle)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 16)
.strokeBorder()
.foregroundColor(.green)
)
You can style SwiftUI Text views with a bunch of dedicated ViewModifiers. Learn about them in this article.
You can switch the font to cursive using the italic
modifier.
Text("italic")
.italic()
The bold
view modifier will make the displayed text bolder.
Text("bold")
.bold()
Of course, you can stack these two modifiers to create an italic and bold typeface.
Text("bold & italic")
.bold()
.italic()
If you want to set your font to something different than bold, you can use the weight
modifier.
Text("fontWeight heavy")
.fontWeight(.heavy)
The font
modifier is applicable if you want to change more text parameters. There are predefined fonts that you can use for various elements of the app. In the example below title
font is set.
Text("font .title")
.font(.title)
You can create own Font struct
to pick different font family, size or weight.
There are two versions of the strikethrough
modifier. One without argument that displays a line in the foreground color:
Text("strikethrough")
.strikethrough()
The second one has an active
parameter and color option where you can control a strikethrough color.
Text("strikethrough color")
.strikethrough(true, color: .red)
Instead of strikethrough, you may want to underline a text. API for this is very similar. More straightforward underline modifier takes foreground color value:
Text("underline")
.underline()
And more advanced has active and color parameters:
Text("underline color")
.underline(true, color: .red)
Text view content color can be changed using the foregroundColor
modifier.
Text("foregroundColor")
.foregroundColor(.red)
The background of any view can be applied using the background
modifier. Since SwiftUI Color is also a view, you can use it to apply a color background to the text below.
Text("background")
.background(Color.red)
The baseline can be used to display your text above or below the normal text baseline. The baselineOffset
view modifier accept positive and negative numbers. You will often use it with Text view concatenation with +
sign.
Text("baseline ")
+ Text("Off")
.baselineOffset(10)
+ Text("set")
.baselineOffset(-10)
You can adjust kerning (the distance between a pair of letters) with the kerning modifier.
Text("kerning kerning kerning")
.kerning(1.2)
Tracking delivers a similar effect to kerning, but it adds distance between each character, which means that ligatures will also be separated.
Text("tracking tracking tracking")
.tracking(1.2)
Swipeable pages, often used for application onboardings, can be easily made using SwiftUI TabView
. In this tutorial, you will learn how to create a view like the one presented in the (low-quality) GIF below.
When you see TabView's
name, the first you may think is a tab bar navigation like the one you can make using UITabBarController
. It is its first and most common usage, but TabView can be styled to look like UIPageViewController
.
TabView
has a straightforward API. Basic usage of it looks like this:
TabView {
Text("Page 1")
Text("Page 2")
Text("Page 3")
}
You list all tabs/pages using view builder. It’s often used with a specification of images and titles placed on the tab bar items:
TabView {
Text("Page 1")
.tabItem {
Image(systemName: "1.square.fill")
Text("Page 1")
}
Text("Page 2")
.tabItem {
Image(systemName: "2.square.fill")
Text("Page 2")
}
Text("Page 3")
.tabItem {
Image(systemName: "3.square.fill")
Text("Page 3")
}
}
To change the tab bar to page view, you have to set a different style using tabViewStyle
modifier:
.tabViewStyle(PageTabViewStyle())
To achieve the effect from GIF, you have to create a Page view first:
import SwiftUI
struct Page: View {
let title: String
let image: String
let color: Color
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
.padding()
Text(title)
.bold()
.font(.title)
.foregroundColor(.white)
.padding(.bottom, 32)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(color)
.cornerRadius(16)
.padding()
}
}
and then configure TabView as follow:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
Page(
title: "Wombat",
image: "wombat",
color: Color(hex: 0xF8CF3C)
)
Page(
title: "Koala",
image: "koala",
color: Color(hex: 0xF6B6AC)
)
Page(
title: "Platypus",
image: "platypus",
color: Color(hex: 0xF26E64)
)
}
.tabViewStyle(PageTabViewStyle())
}
}
ZStack
is one of the basic building blocks of SwiftUI applications. Along with HStack
and VStack
, you will use them frequently. This article will explain what ZStack
is and how to use it to build a crucial part of every chat UI - a chat bubble.
So what is this view with the strange name ZStack
? It’s a view that arranges its children one into another/overlays them. In 3D space, we usually mark the third direction z
along x
and y
.
ZStack
syntax is very similar to HStack
and VStack
.
ZStack {
Circle()
.foregroundColor(.green)
Text("Swift Wombat")
}
Additionally, you can provide an alignment parameter that controls the view position on a horizontal and vertical axis.
ZStack(
alignment: Alignment(horizontal: .leading, vertical: .top)
) {
Circle()
.foregroundColor(.green)
Text("Swift Wombat")
}
There is one view modifier that you need to know to complete this task. It’s called a layoutPriority
, and by providing a Double
value for it, you can control the importance of views when the stack will layout them. By default, each has a priority set to zero. Set it to a higher value, and it will be authoritative when the stack calculates its frame.
You may wonder why this is important. To make a chat bubble, you have to use Image
view as a background with resizable
modifier.
Learn more about resizable and how to control Image resize zones with it
Resizable means that it will try to take all available space, but you want the background to fit the given text. It is a point where you should use layoutPriority
.
ZStack {
Image("bubble")
.resizable(capInsets: EdgeInsets(top: 39, leading: 49, bottom: 39, trailing: 49))
.renderingMode(.template)
.foregroundColor(.blue)
Text("Did you hear about the Swift Wombat website?")
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical)
.layoutPriority(1)
}
That way, the Text
view will have a layout priority, and the Image
will adjust to its size.
In the Xcode Playground example linked at the bottom of this article, you will find full code that will adjust the color and orientation of bubbles to distinguish messages that we send and receive.
I saw the “How to optionally make a Text italic?” question on Reddit, and I thought it would be good material for a short article. From this tutorial, you will learn how to prepare a conditional italic modifier. Additionally, you will find out how to make more generic modifiers to control every Text’s properties.
So, let’s start with italic. Let’s assume that you have a @State property wrapper that looks like this one:
@State private var isItalic = true
You can use the if ... else ...
statement to control which version of Text is displayed.
if isItalic {
Text("Swift Wombat")
.italic()
} else {
Text("Swift Wombat")
}
This way, you have to repeat a code, which you should try to avoid.
Some text modifiers (strikethrough for example) have an active
parameter, but not italic
. You can easily create that with this extension:
import SwiftUI
extension Text {
func italic(_ active: Bool) -> Text {
guard active else { return self }
return italic()
}
}
And this alone will be an answer to the question from the first paragraph. But what if you want to handle also bold or font modifiers? You can iterate at the solution above and add an additional modifier to the Text extension.
import SwiftUI
extension Text {
func active(
_ active: Bool,
_ modifier: (Text) -> Text
) -> Text {
guard active else { return self }
return modifier(self)
}
}
It is a bit harder to read. This function takes two arguments. One is the active state that controls if modifier is applied on not, and the second one is a function - which gets a Text struct and returns its modified version. You can use it like this:
Text("Swift Wombat")
.active(isTitle, { $0.font(.title) })
For functions without arguments, you may prepare another extension:
import SwiftUI
extension Text {
func active(
_ active: Bool,
_ modifier: (Text) -> () -> Text
) -> Text {
guard active else { return self }
return modifier(self)()
}
}
With it, you will be able to apply italic or bold even quicker:
Text("Swift Wombat")
.active(isItalic, Text.italic)
.active(isBold, Text.bold)
Note the capital case of the struct name Text
in that version.
By default, @AppStorage
property wrapper supports Int
, String
, Double
, Data
, or URL
values. You can store other classes/structs/enums if you make them conform to the RawRepresentable
protocol. This tutorial will learn how to keep Dates in UserDefaults
handled by @AppStorage
.
If you write something like this:
@AppStorage("savedDate") var date: Date = Date()
the Swift compiler gives you an error because the @AppStorage
property wrapper doesn’t support Date type.
But it supports objects that conform to RawRepresentable
protocol, where that raw value is a String or Int. A proposed implementation may look like this:
import Foundation
extension Date: RawRepresentable {
private static let formatter = ISO8601DateFormatter()
public var rawValue: String {
Date.formatter.string(from: self)
}
public init?(rawValue: String) {
self = Date.formatter.date(from: rawValue) ?? Date()
}
}
This code uses ISO8601DateFormatter
to format a date to String and map it back. That formatter is static because creating and removing DateFormatters
is an expensive operation. If you add the code above to your project, you will be able to read and store dates in your SwiftUI app.
struct DateView: View {
@AppStorage("savedDate") var date: Date = Date()
var body: some View {
VStack {
Text(date, style: .date)
Text(date, style: .time)
Button("Save date") { date = Date() }
}
}
}
SwiftUI views in larger applications may use some configuration passed to them as environment objects. It is a very beneficial way when the config is needed somewhere deep into a view tree. If your view uses the @EnvironmentObject
property wrapper, you can still debug and tweak it using live preview functionality.
Take a look at this example. In the article about home screen Quick Actions in SwiftUI, I’ve used ObservableObject
called QuickActionService
. You can use it to pass selected Quick Action to the given view.
import Foundation
enum QuickAction: String {
case newMessage, search, inbox
}
final class QuickActionService: ObservableObject {
@Published var action: QuickAction? = nil
}
When you provide that object using environmentObject
modifier, you can listen to changes in it in any place down the view tree.
private let quickActionService = QuickActionService()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(quickActionService)
}
}
Live previews, though, are rendered without that context of upper views. So you have to use the same environmentObject
modifier in your preview code.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(QuickActionService())
}
}
With a slight modification of QuickActionService
:
final class QuickActionService: ObservableObject {
@Published var action: QuickAction?
init(initialValue: QuickAction? = nil) {
action = initialValue
}
}
you will be able to test different versions of your screen based on different values for quick action parameters.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environmentObject(QuickActionService())
ContentView()
.environmentObject(QuickActionService(initialValue: .newMessage))
}
}
}