Categories
iOS Swift SwiftUI

Ratings view in SwiftUI

I am currently building a new app where I needed to show ratings for items. Ratings are from 0 to 5 with 0.5 step. In this blog post, we’ll implement a SwiftUI view which uses SF symbols to display ratings. Since we need to make sure it works properly with each of the step, we are going to leverage the usefulness of SwiftUI previews and render each state one by one vertically. Swift has a function called stride what we can use to easily create an array of double values with a step of 0.5 and feed it in to ForEach.

struct RatingsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
let steps = Array(stride(from: 0.0, through: 5.0, by: 0.5))
ForEach(steps, id: \.self) { value in
RatingsView(value: value)
}
}
.previewLayout(.sizeThatFits)
}
}
view raw Preview.swift hosted with ❤ by GitHub

The view needs to render 5 stars next to each other and figure out which stars are filled, half filled or empty. Rendering 5 images is straight-forward in SwiftUI. We can use HStack and ForEach with an integer range from 0 to 5. Since we use SF symbols, then the colour of the filled star can be applied with the foregroundColor view modifier.

/// A view displaying a star rating with a step of 0.5.
struct RatingsView: View {
/// A value in range of 0.0 to 5.0.
let value: Double
var body: some View {
HStack(spacing: 0) {
ForEach(0..<5) { index in
Image(systemName: imageName(for: index, value: value))
}
}
.foregroundColor(.yellow)
}
func imageName(for starIndex: Int, value: Double) -> String {
// redacted
}
}

The most complicated part is implementing a function what returns the name of the SF symbol for the current star index and the double value passed into the view. When I thought about it then I ended up with two solutions: one with if clauses and the other one with switch statement.

func imageName(for starIndex: Int, value: Double) -> String {
// Version A
if value >= Double(starIndex + 1) {
return "star.fill"
}
else if value >= Double(starIndex) + 0.5 {
return "star.leadinghalf.filled"
}
else {
return "star"
}
// Version B
switch value Double(starIndex) {
case ..<0.5: return "star"
case 0.5..<1.0: return "star.leadinghalf.filled"
default: return "star.fill"
}
}

The solution A is checking if the value is larger than the star index + 1 which means that in case of value 1.0 the zero indexed star image is rendered as filled star. Half-filled cases are handled by checking if the zero indexed star index + 0.5 is less than the current value. The solution B uses open-ended ranges and a switch statement. When we subtract the zero indexed star index from the current value, then if it is less than 0.5 then the star should be not filled, if 0.5 to 1.0 then half-filled and in other cases filled. When comparing these implementations, then the B is more concise, but on the other hand A is more readable. Years ago I read a book “Masters of Doom” and John Romero’s programming principles. Don’t know why, but his programming principle “simplify” always pops on my mind when I need to decide on multiple implementations. Like here, only because of this, I am going for solution A.

SwiftUI preview showing each state of the ratings view.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Getting started with matched geometry effect in SwiftUI

Matched geometry effect in SwiftUI is a view modifier for creating transition animations between two views. We create two different views, apply the modifier to both of them with a same identifier and namespace, and removing one and inserting another will create an animation where one view moves to the other view’s position. In other words, the view slides from the removed view’s position to the inserted view’s position. Let’s have a look at a concrete example, where we have two rows of views: rectangles and circles. Tapping on any of the views will remove it from one row and insert it into another row. Since we use matchedGeometryEffect view modifier, the change is animated and one view slides to another row.

The view implementation is straight forward. We have a view which renders two rows with ForEach and each row element is a button what has matchedGeometryEffect view modifier applied to. Model items just have an id and colour which is used for setting the foregroundColor. The view model holds two arrays of items, and select methods just remove one item from one array and insert it in another.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
@Namespace var colorSelectionNamespace
var body: some View {
VStack {
HStack {
ForEach(viewModel.topColors) { item in
Button(action: { viewModel.selectTopColor(item) }) {
Rectangle()
.foregroundColor(item.color)
.frame(width: 40, height: 40)
}
.matchedGeometryEffect(id: item.id, in: colorSelectionNamespace)
}
}
.frame(minHeight: 50)
Spacer()
.frame(height: 200)
HStack {
ForEach(viewModel.bottomColors) { item in
Button(action: { viewModel.selectBottomColor(item) }) {
Circle()
.foregroundColor(item.color)
.frame(width: 20, height: 20)
}
.matchedGeometryEffect(id: item.id, in: colorSelectionNamespace)
}
}
.frame(minHeight: 50)
}
}
}

The view model’s implementation is shown below. Since we want to animate changes, we use withAnimation block and inside the block just remove one item and then insert it to another. Item properties use @Published property wrapper and the view model is ObservableObject, then property changes trigger view refresh. As one view is removed and the other view is inserted, then matchedGeometryEffect will trigger a transition animation where the view moves from one position to another with default fade animation.

extension ContentView {
final class ViewModel: ObservableObject {
@Published var topColors = ColorItem.all
@Published var bottomColors = [ColorItem]()
func selectBottomColor(_ item: ColorItem) {
withAnimation {
guard let index = bottomColors.firstIndex(of: item) else { return }
bottomColors.remove(at: index)
let insertionIndex = (topColors.startIndextopColors.endIndex).randomElement() ?? 0
topColors.insert(item, at: insertionIndex)
}
}
func selectTopColor(_ item: ColorItem) {
withAnimation {
guard let index = topColors.firstIndex(of: item) else { return }
topColors.remove(at: index)
let insertionIndex = (bottomColors.startIndexbottomColors.endIndex).randomElement() ?? 0
bottomColors.insert(item, at: insertionIndex)
}
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

Implicit self for weak self captures

Since this week has kept me extremely busy with packing our things, selling furniture, and wrapping up any loose ends before relocating back to Estonia from Japan after 4.5 years, I am going to use this week’s blog post for highlighting a new Swift 5.8 language feature. I welcome this change since how hard I try there still will be cases where I need to capture self in closures.

The language feature we are talking about is of course SE-0365: Allow implicit self for weak self captures, after self is unwrapped. Which means that if we capture self weakly in a closure, use guard let self then there is no need to write self. inside the closure any more.

Let’s take a look at a concrete example from my SignalPath app. It is a tiny change, but I feel like it makes a lot of sense. Especially because I already have explicitly handled weak self with guard let.

// Old
iqDataSource.didChangeSampleCount
.sink { [weak self] sampleCount in
guard let self else { return }
let seriesDelta = self.layout.adjust(to: sampleCount)
guard seriesDelta < 0 else { return }
self.resetTiles(priority: .immediately)
}
.store(in: &cancellables)
// New
iqDataSource.didChangeSampleCount
.sink { [weak self] sampleCount in
guard let self else { return }
let seriesDelta = layout.adjust(to: sampleCount)
guard seriesDelta < 0 else { return }
resetTiles(priority: .immediately)
}
.store(in: &cancellables)
view raw Snippet.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Bizarre error in SwiftUI preview

The other day, I was playing around with matchedGeometryEffect view modifier in my sample app. I was just planning to show a list of items and then animate moving one item from one HStack to another. Suddenly, my SwiftUI preview stopped working. On the other hand, running exactly the same code on the simulator just worked fine. The code was very simple, consisting of view, view model and an Item model struct.

import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(viewModel.items) { item in
Text(verbatim: item.name)
}
}
.padding()
}
}
extension ContentView {
final class ViewModel: ObservableObject {
let items: [Item] = [
Item(name: "first"),
Item(name: "second")
]
func select(_ item: Item) {
// implement
}
}
struct Item: Identifiable {
let name: String
var id: String { name }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

If you try to render SwiftUI preview (I was using Xcode 14.3) then Xcode is giving up with “Failed to launch the app XXX in reasonable time”. But if I try to build and run it on simulator, it just works fine. After some trial and error, it turned out that SwiftUI previews broke as soon as I added the func select(_ item: Item) function. If you pay a close attention, then you can see that the longer type name for Item is ContentView.Item, but within the ContentView.ViewModel type I am using just Item. I do not know why, but SwiftUI previews seems to get confused by it. As soon as I change the function declaration to func select(_ item: ContentView.Item) the preview starts rendering again. Another way is declaring the Item struct outside the ContentView extension.

The learning point is that if SwiftUI previews stop working suddenly, then make sure to check how nested types are used in function declarations.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
CoreData iOS Swift SwiftUI

NSFetchedResultsController wrapper for SwiftUI view models

Although SwiftUI framework contains a @FetchRequest property wrapper, it is not possible to use it outside of SwiftUI views because it relies on accessing the managed object context from the view environment. While working on an CoreData based app which uses view models a lot within SwiftUI views, I ended up creating a wrapper class for making it easier to use NSFetchedResultsController. The wrapper class is named FetchedResultList and what it does internally is creating a NSFetchedResultsController instance, handling its delegate methods, and notifying about data changes through closures. Here is an example of a view model using the wrapper.

@MainActor final class ViewModel: ObservableObject {
private let list: FetchedResultList<Fruit>
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
list = FetchedResultList(context: context,
sortDescriptors: [
NSSortDescriptor(keyPath: \Fruit.name, ascending: true)
])
list.willChange = { [weak self] in self?.objectWillChange.send() }
}
var fruits: [Fruit] {
list.items
}
@Published var searchText: String = "" {
didSet {
if searchText.isEmpty {
list.predicate = nil
}
else {
list.predicate = NSPredicate(format: "name contains[cd] %@", searchText)
}
}
}
}
view raw Fruits.swift hosted with ❤ by GitHub

The view model conforms to ObservableObject protocol and FetchedResultList provides a willChange closure which gets called when NSFetchedResultsController’s will change delegate is called. Calling the objectWillChange publisher will signal the SwiftUI view about the data change and the view gets re-rendered. The wrapper also supports updating predicate or sort descriptors dynamically. Here, we can see how setting a searchableText property from the view will update the predicate of the wrapper class. The SwiftUI view which uses the view model looks like this:

struct FruitList: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack {
List(viewModel.fruits) { item in
NavigationLink(item.name) {
Text(verbatim: "Detail view for \(item.name)")
}
}
.navigationTitle("Fruits")
.searchable(text: $viewModel.searchText)
}
}
}
view raw Fruits.swift hosted with ❤ by GitHub

Let’s take a look at how the wrapper class is implemented. The core logic around the wrapper is creating an instance of NSFetchedResultsController, allowing to reconfigure it dynamically, handling its delegate, and notifying changes through closures. Using closures instead of conforming to ObservableObject is a conscious choice since the main use case is using the wrapper class in view models or in other controllers, and it means that propagating the data change to a SwiftUI view needs to be done manually. It is shorter to call view model’s objectWillChange in a closure than republishing wrapper’s objectWillChange to view model’s objectWillChange. Moreover, it would make more sense to use SwiftUI provided property wrapper instead of this wrapper if we would want to handle CoreData fetching in the SwiftUI view’s implementation.

@MainActor final class FetchedResultList<Result: NSManagedObject> {
private let fetchedResultsController: NSFetchedResultsController<Result>
private let observer: FetchedResultsObserver<Result>
init(context: NSManagedObjectContext, filter: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]) {
let request = NSFetchRequest<Result>(entityName: Result.entity().name ?? "<not set>")
request.predicate = filter
request.sortDescriptors = sortDescriptors.isEmpty ? nil : sortDescriptors
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
observer = FetchedResultsObserver(controller: fetchedResultsController)
observer.willChange = { [unowned self] in self.willChange?() }
observer.didChange = { [unowned self] in self.didChange?() }
refresh()
}
private func refresh() {
do {
try fetchedResultsController.performFetch()
}
catch {
Logger().error("Failed to load results")
}
}
var items: [Result] {
fetchedResultsController.fetchedObjects ?? []
}
var predicate: NSPredicate? {
get {
fetchedResultsController.fetchRequest.predicate
}
set {
fetchedResultsController.fetchRequest.predicate = newValue
refresh()
}
}
var sortDescriptors: [NSSortDescriptor] {
get {
fetchedResultsController.fetchRequest.sortDescriptors ?? []
}
set {
fetchedResultsController.fetchRequest.sortDescriptors = newValue.isEmpty ? nil : newValue
refresh()
}
}
var willChange: (() -> Void)?
var didChange: (() -> Void)?
}

The wrapper class uses private FetchedResultsObserver class which must derive from NSObject because it implements NSFetchedResultsControllerDelegate methods. This approach allows keeping the FetchedResultList class a pure Swift class and not a NSObject subclass which I like to avoid in SwiftUI apps (just a personal preference).

private final class FetchedResultsObserver<Result: NSManagedObject>: NSObject, NSFetchedResultsControllerDelegate {
var willChange: () -> Void = {}
var didChange: () -> Void = {}
init(controller: NSFetchedResultsController<Result>) {
super.init()
controller.delegate = self
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
willChange()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
didChange()
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

Async-await and completion handler compatibility in Swift

The prominent way for writing async code before async-await arrived to Swift was using completion handlers. We pass in a completion handler which then gets called at some later time. When working with larger codebases, it is not straight-forward to convert existing code to use newer techniques like async-await. Often we make these changes over time, which means that in case of wrapping completion handler based code, we would have the same function both in form of completion handler and async await. Fortunately, it is easy to wrap existing completion handler based code and to provide an async-await version. The withCheckedThrowingContinuation() function is exactly for that use-case. It provides an object which will receive the output of our completion handler based code – most of the time a value or an error. If we use Result type in completion handlers, then it is only 3 lines of code to wrap it, thanks to the fact that the continuation has a dedicated function for resuming result types.

final class ImageFetcher {
func fetchImages(for identifiers: [String], completionHandler: @escaping (Result<[String: UIImage], Error>) -> Void) {
//
}
}
extension ImageFetcher {
func fetchImages(for identifiers: [String]) async throws -> [String: UIImage] {
try await withCheckedThrowingContinuation { continuation in
fetchImages(for: identifiers) { result in
continuation.resume(with: result)
}
}
}
}

Great, but what if we add new code to an existing code base relying heavily on completion handler based code? Can we start with an async function and wrap that as well? Sure. In the example below, we have some sort of DataFetcher which has an async function. If we needed to call this function from a completion handler based code, we can add a wrapping function pretty easily. Later, if we have fully converted to async-await, it can be discarded easily. So how do we do it? We start off the wrapping code by creating a Task which starts running automatically and which also provides an async context for calling async functions. This means that we can call the async function with try await and catching the error if it throws. Then it is just a matter of calling the completion handler. Depends on the use-case and how this code is meant to be used, but we should always think about which thread should be calling the completion handler. In the example, we always switch to the main thread because the Task’s closure is running on a global actor (in other words, on a background thread).

final class DataFetcher {
func fetchData(for identifiers: [String]) async throws -> [String: Data] {
//
}
}
extension DataFetcher {
func fetchData(for identifiers: [String], completionHandler: @escaping (Result<[String: Data], Error>) -> Void) {
Task {
do {
let data = try await fetchData(for: identifiers)
await MainActor.run {
completionHandler(.success(data))
}
}
catch {
await MainActor.run {
completionHandler(.failure(error))
}
}
}
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

TaskGroup error handling in Swift

Task groups in Swift are used for running n-number of child tasks and letting it handle things like cancellation or different priorities. Task groups are created with either withThrowingTaskGroup(of:returning:body:) or withTaskGroup(of:returning:body:). The latter is for cases when errors are not thrown. In this blog post, we will observe two cases of generating Data objects using a task group. In the first case, we want to stop the group as soon as an error has occurred and discard all the remaining work. The other case looks at ignoring any errors in child tasks and collect just collecting Data objects for tasks which were successful.

The example we are going to use simulates creating image data for multiple identifiers and then returning an array of Data objects. The actual image creating and processing is simulated with Task’s sleep function. Since task groups coordinate cancellation to all the child tasks, then the processor implementation also calls Task.checkCancellation() to react to cancellation and stopping as soon as possible for avoiding unnecessary work.

struct ImageProcessor {
static func process(identifier: Int) async throws -> Data {
// Read image data
try await Task.sleep(nanoseconds: UInt64(identifier) * UInt64(1e8))
try Task.checkCancellation()
// Simulate processing the data and transforming it
try await Task.sleep(nanoseconds: UInt64(1e8))
try Task.checkCancellation()
if identifier != 2 {
print("Success: \(identifier)")
return Data()
}
else {
print("Failing: \(identifier)")
throw ProcessingError.invalidData
}
}
enum ProcessingError: Error {
case invalidData
}
}

Now we have the processor created. Let’s see an example of calling this function from a task group. As soon as we detect an error in one of the child tasks, we would like to stop processing and return an error from the task group.

let imageDatas = try await withThrowingTaskGroup(of: Data.self, returning: [Data].self) { group in
imageIdentifiers.forEach { imageIdentifier in
group.addTask {
return try await ImageProcessor.process(identifier: imageIdentifier)
}
}
var results = [Data]()
for try await imageData in group {
results.append(imageData)
}
return results
}
view raw Case1.swift hosted with ❤ by GitHub

We loop over the imageIdentifiers array and create a child task for each of these. When child tasks are created and running, we wait for child tasks to finish by looping over the group and waiting each of the child task. If the child task throws an error, then in the for loop we re-throw the error which makes the task group to cancel all the remaining child tasks and then return the error to the caller. Since we loop over each of the task and wait until it finishes, then the group will throw an error of the first added failing task. Also, just to remind that cancellation needs to be handled explicitly by the child task’s implementation by calling Task.checkCancellation().

Great, but what if we would like to ignore errors in child tasks and just collect Data objects of all the successful tasks. This could be implemented with withTaskGroup function by specifying the child task’s return type optional and handling the error within the child task’s closure. If error is thrown, return nil, and later when looping over child tasks, ignore nil values with AsyncSequence’s compactMap().

let imageDatas = await withTaskGroup(of: Data?.self, returning: [Data].self) { group in
imageIdentifiers.forEach { imageIdentifier in
group.addTask {
do {
return try await ImageProcessor.process(identifier: imageIdentifier)
} catch {
return nil
}
}
}
var results = [Data]()
for await imageData in group.compactMap({ $0 }) {
results.append(imageData)
}
return results
}
view raw Case2.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Grid view in SwiftUI

SwiftUI framework got a new addition in iOS 16 named Grid which is enables creating grid layouts in a quick way. The main difference between using Grid instead of combining HStack and VStack is that all the cells will get the same size. SwiftUI will create all the cells, measure the size, and apply the largest size to all the cells. Here is an example of a grid which in addition to cells have additional accessory views.

struct ContentView: View {
var body: some View {
Grid(alignment: .center,
horizontalSpacing: 16,
verticalSpacing: 8) {
Separator(title: "Today")
.gridCellUnsizedAxes(.horizontal)
GridRow {
Text("Finals")
Cell(title: "Archery", imageName: "figure.archery")
Cell(title: "Badminton", imageName: "figure.badminton")
}
Separator(title: "Tomorrow")
.gridCellUnsizedAxes(.horizontal)
GridRow(alignment: .bottom) {
Text("Qualifications")
Cell(title: "Bowling", imageName: "figure.archery")
Cell(title: "Golf", imageName: "figure.golf")
Cell(title: "Handball", imageName: "figure.handball")
}
}
}
}
view raw Grid.swift hosted with ❤ by GitHub

In the example above we can see how Grid is created: first we have the Grid container view with one or multiple GridRow views which represents a single row of cells. If we want to decorate the grid with accessory views, we can just add more views which are not wrapped into GridRow. Separator view is just a view which displays text and a divider. The gridCellUnsizedAxes() allows controlling how these accessory views are laid out. Separator contains a Divider, which is a flexible view and wants to take as much space it could. By applying the view modifier we can disable this behaviour and the width of the accessory view is not limited by the number of columns in the grid.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI

Presenting multiple sheets in SwiftUI

SwiftUI has multiple view modifiers for presenting sheets. If we just want to present a modal view, then we can use one of these:

func sheet<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
content: @escaping () -> Content
) -> some View where Content : View
func sheet<Item, Content>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
content: @escaping (Item) -> Content
) -> some View where Item : Identifiable, Content : View
view raw Sheets.swift hosted with ❤ by GitHub

The first requires a boolean binding, whereas the second an identifiable item. When dealing with views which need to present different views in a sheet, then the latter can be easily expanded to support that. We can create an enum, conform it to Identifiable and then add an optional @State property which selects the view we should be presenting. The Identifiable protocol requires implementing an id property, which we can easily do by reusing rawValue property of an enum with raw types. If we put all of this together, then we can write something like this:

struct ContentView: View {
enum Sheet: String, Identifiable {
case addItem, settings
var id: String { rawValue }
}
@State private var sheet: Sheet?
var body: some View {
VStack {
Button("Add Item", action: { sheet = .addItem })
Button("Settings", action: { sheet = .settings })
}
.sheet(item: $sheet, content: makeSheet)
}
@ViewBuilder
func makeSheet(_ sheet: Sheet) -> some View {
switch sheet {
case .addItem:
AddItemView()
case .settings:
SettingsView()
}
}
}

In the example above, I also separated the sheet view creation by having a separate function with an argument of type ContentView.Sheet. Since the function returns views with different types, then it needs to be annotated with @ViewBuilder. All in all it is a pretty concise and gives a nice call site where we just assign a sheet identifier to the sheet property.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Foundation iOS Swift

URL type properties for folders in iOS

I have a tiny Swift package, what I have been using for reading and writing data on disk. Data is written to a subfolder in the documents folder. Beginning with iOS 16 there is a new way how to create that URL. It is such a tiny addition to what should have been there a long time ago.

let url = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
// vs
let url2 = URL.documentsDirectory
view raw URL.swift hosted with ❤ by GitHub

Here we can see that instead of a throwing function which has 4 parameters, we can replace it with a non-throwing type property. Finally! Not sure why it gives me so much happiness, maybe because I always forget the URL API whenever I need it.

static var applicationDirectory: URL
static var applicationSupportDirectory: URL
static var cachesDirectory: URL
static var desktoDirectory: URL
static var documentsDirectory: URL
static var downloadsDirectory: URL
static var homeDirectory: URL
static var libraryDirectory: URL
static var moviesDirectory: URL
static var musicDirectory: URL
static var picturesDirectory: URL
static var sharedPublicDirectory: URL
static var temporaryDirectory: URL
static var trashDirectory: URL
static var userDirectory: URL
view raw URL.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.