Issue #928
For iOS, use string
Setting this property replaces all current items in the pasteboard with the new item. If the first item has no value of the indicated type, nil is returned.
let pasteboard = UIPasteboard.general pasteboard.string = "hello world" For Mac, use clearContents first
Clears the existing contents of the pasteboard, preparing it for new contents. This is the first step in providing data on the pasteboard....
Issue #925
Use AES.GCM method with 128 bits key
import CryptoKit public extension Optional { func tryUnwrap() throws -> Wrapped { if let value = self { return value } else { throw NSError(domain: "", code: 0) } } } public struct Crypto { static func encrypt(input: Data, key: String) -> Data { do { let keyData = Data(key.data(using: .utf8)!.prefix(32)) let key = SymmetricKey(data: keyData) let sealed = try AES.GCM.seal(input, using: key) return try sealed....
Issue #924
Note
Navigation state needs to be in the container of NavigationSplitView for changes to propagate Need to use WindowGroup for navigation bar to work NavigationSplitView
the navigation split view coordinates with the List in its first column, so that when people make a selection, the detail view updates accordingly. Programmatic changes that you make to the selection property also affect both the list appearance and the presented detail view...
Issue #923
NavigationLink on Mac applies the default button style. We can style it using ButtonStyle, here to use plain style we can just
NavigationLink(value: DetailRoute.books) { BooksView() } .buttonStyle(.plain)
Issue #921
Use HStack with TextField and a little extension
extension Binding where Value == Int { var toString: Binding<String> { Binding<String>( get: { "\(wrappedValue)" }, set: { wrappedValue = Int($0) ?? 0 } ) } } struct TextFieldStepper: View { @Binding var value: Int var body: some View { HStack(spacing: 0) { TextField("", text: $value.toString) .textFieldStyle(.roundedBorder) .frame(width: 50) Stepper("", value: $value) } } }
Issue #913
Use QuickLookThumbnailing framework
import AppKit import QuickLookThumbnailing actor QuicklookService { static let shared = QuicklookService() private let generator = QLThumbnailGenerator.shared func image( fileUrl: URL, size: CGSize ) async -> NSImage { let scale = NSScreen.main?.backingScaleFactor ?? 2 let request = QLThumbnailGenerator.Request( fileAt: fileUrl, size: size, scale: scale, representationTypes: .thumbnail ) do { let representation = try await generator.generateBestRepresentation(for: request) return representation.nsImage } catch { return NSWorkspace.shared.icon(forFile: fileUrl.path) } } } Read more Creating Quick Look Thumbnails to Preview Files in Your App
Issue #912
Perform check before and after suspension point
actor Worker { var isDoing = false var toBeDone = Set<String>() func work(string: String) async { if isDoing { toBeDone.insert(string) return } isDoing = true await performHeavyWork(string: string) isDoing = false if let first = toBeDone.popFirst() { await work(string: first) } } private func performHeavyWork(string: String) async { try? await Task.sleep(nanoseconds: 5_000_000_000) print(string) } } func main() { let worker = Worker() Array(0 ....
Issue #911
Make an parallelTask function that wraps TaskGroup
public func parallelTask(@ParallelTaskBuilder builder: () -> [ParallelTaskBuilder.Work]) async { await withTaskGroup(of: Void.self) { group in for work in builder() { group.addTask { await work.value } } } } @resultBuilder public struct ParallelTaskBuilder { public typealias Work = Task<Void, Never> public static func buildExpression(_ expression: Work?) -> [Work] { if let expression = expression { return [expression] } return [] } public static func buildExpression(_ expression: Work) -> [Work] { [expression] } public static func buildExpression(_ expression: [Work]) -> [Work] { expression } public static func buildBlock(_ components: Work....
Issue #910
Use one-sided range operator let string = "Hello world" string[string.startIndex...] // Hello world string[..<string.endIndex] // Hello world Substring let string = "Hello world" let range = string.startIndex ..< string.index(string.startIndex, offsetBy: 5) string[range] // Hello Convert to and from NSRange let string = "Hello world" let range = string.startIndex... let nsRange = NSRange(range, in: string) let regex = NSRegularExpression(pattern: pattern) let matches = regex.matches(in: string, range: nsRange) for match in matches { let range = Range(match....
Issue #908
When we add another UIWindow, then its rootViewController will decide the style of the status bar, not the rootViewController of the keyWindow anymore
childForStatusBarStyle The usual way to fix this is to defer the decision to the correct rootViewController, like in our HUDViewController
class HUDViewController: UIViewController { override var childForStatusBarStyle: UIViewController? { let windows = view.window?.windowScene?.windows ?? [] for window in windows where window != self.view.window { return window....
Issue #907
Read more https://swdevnotes.com/swift/2022/customise-a-line-chart-with-swiftui-charts-in-ios-16/ https://github.com/jordibruin/Swift-Charts-Examples
Issue #905
Protect mutable state with Swift actors
Actor reentrancy
Imagine we have two different concurrent tasks trying to fetch the same image at the same time. The first sees that there is no cache entry, proceeds to start downloading the image from the server, and then gets suspended because the download will take a while. While the first task is downloading the image, a new image might be deployed to the server under the same URL....
Issue #904
Consider this code where we have an ObservableObject with fetch1 and async fetch2, and a fetch inside ContentView
Here the observation in Xcode 14
ViewModel.fetch1: run on main thread ViewModel.fetch2: run on cooperative thread pool ContentView.fetch: run on main thread import SwiftUI import CoreData import Combine class ViewModel: ObservableObject { @Published var string = "" func fetch1() { let url = URL(string: "https://google.com")! let data = try!...
Issue #903
let image = NSImage(contentsOf: url) let imageView = NSImageView(image: image) image.animates = true
Issue #900
Listen to didActivateApplicationNotification and check that it is not our app
NSWorkspace.shared.notificationCenter .publisher(for: NSWorkspace.didActivateApplicationNotification) .sink(receiveValue: { [weak self] note in guard let app = note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier != Bundle.main.bundleIdentifier else { return } self?.frontMostApp = app }) .store(in: &bag)
Issue #899
Use NSTitlebarAccessoryViewController
var titleBarAccessoryVC: NSTitlebarAccessoryViewController { let vc = NSTitlebarAccessoryViewController() let view = HStack { Spacer() Button { } label: { Text("Save") } .buttonStyle(.borderedProminent) .controlSize(.large) } .padding(.horizontal) vc.view = NSHostingView(rootView: view) return vc } let window: NSWindow = ... window.addTitlebarAccessoryViewController(titleBarAccessoryVC)
Issue #898
Change element position using either offset or position, and use DragGesture
Use GestureState to store the updating startDragLocation to keep the start location when drag begins, so we can add translation
struct MoveModifier: ViewModifier { @Binding var position: CGPoint @GestureState private var startLocation: CGPoint? func body(content: Content) -> some View { content .gesture(gesture) } private var gesture: some Gesture { DragGesture() .onChanged { value in var position = startLocation ?...
Issue #896
Use underscore _focus we get access to underlying FocusState object, but underscore _ is private to a View hence can鈥檛 be used in extension
If we want to pass FocusState to another View or in extension, we can pass its Binding
enum FocusElement: Hashable { case name case email } struct ParentView: View { @FocusState var focus: FocusElement? var body: some View { ChildView1(focus: _focus) ChildView2(focus: $focus) } } struct ChildView1: View { @FocusState var focus: FocusElement?...
Issue #895
Apply .move on reversed array
List(selection: $viewModel.selectedBook) { ForEach(viewModel.books.reversed()) { book in BookCell(book: book) } .onMove { source, dest in var reversed = Array(viewModel.books.reversed()) reversed.move(fromOffsets: source, toOffset: dest) viewModel.books = reversed.reversed() } }
Issue #891
Below are my favorite WWDC videos. My focus is to use SwiftUI to make useful apps with great UX. I don鈥檛 pay much attention to new frameworks as they come and go, but the underlying reasons and design principles are worth remembering.
WWDC23 The SwiftUI cookbook for focus Discussing some of the things that you can do with focus APIs in SwiftUI to cook up a really great user experience...