gshaw / notes Goto Github PK
View Code? Open in Web Editor NEWIssues and solutions I find during software development.
Home Page: https://gshaw.ca
License: MIT License
Issues and solutions I find during software development.
Home Page: https://gshaw.ca
License: MIT License
I was running into some annoying bugs with my SwiftUI app around restoring NavigationStack path from SceneStorage. I tried following Paul's [1] and Majid's [2] articles but was still running into problems.
To solve the problem I created a test app that loosely follows the app I was building. It includes a couple of questionable navigation ideas around changing the active tab from a tab view and needing to save two shallow navigation paths.
During testing with both ideas I was noticing that when the views were getting stored the navigation path would be restored from SceneStorage and then immediately updated with an empty array. This was puzzling as I couldn't understand why that array was getting assigned []
. Using the debugger provided no insight.
I eventually thought it might be related to threading and guarded the initial assignment of the restored thread in a @MainActor
Task and that solved the issue.
The example app includes a pattern where SceneStorage is encapsulated at the top of the app view using a MainScene. All the logic around scene state can saved on that view but is made available via a SceneState object that is injected into the environment. Using this pattern views deep in the app are able to manipulate the view state of the app without having to pass bindings through all the views.
I'm not convinced this is the best way to accomplish this but I haven't found anything better. If anybody reads this please leave a comment if you've encountered the problem and/or have any suggestions.
import SwiftUI
final class NavigationStore<Route: Hashable> where Route: Codable {
static func save(path: [Route]) -> Data? {
do {
return try JSONEncoder().encode(path)
} catch {
print("NavigationStore: save error: \(error)")
return nil
}
}
static func restore(data: Data?) -> [Route] {
guard let data else {
return []
}
do {
let representation = try JSONDecoder().decode([Route].self, from: data)
return [Route](representation)
} catch {
print("NavigationStore: restore error: \(error)")
return []
}
}
}
struct Layer: Identifiable {
let id: UUID
let name: String
}
struct Item: Identifiable {
let id: UUID
let name: String
}
class AppData: NSObject, ObservableObject {
var layers = [
Layer(id: UUID(uuidString: "1b927f0c-6d91-49ce-807a-c05dd1fbf2be")!, name: "One Bravo Niner"),
Layer(id: UUID(uuidString: "27801b7f-f6cf-4add-98c9-71f581555996")!, name: "Two Seven Eight"),
Layer(id: UUID(uuidString: "3fec91ad-1889-40da-a630-cccf30a3db2a")!, name: "Three Foxtrot Echo"),
]
var items = [
Item(id: UUID(uuidString: "ab927f0c-6d91-49ce-807a-c05dd1fbf2be")!, name: "Alpha Beta"),
Item(id: UUID(uuidString: "b7801b7f-f6cf-4add-98c9-71f581555996")!, name: "Bravo Seven"),
]
func findItem(itemID: UUID) -> Item? {
items.first { $0.id == itemID }
}
func findLayer(layerID: UUID) -> Layer? {
layers.first { $0.id == layerID }
}
}
@main
struct AppMain: App {
@StateObject private var appData: AppData
var body: some Scene {
WindowGroup {
MainScene()
.environmentObject(appData)
}
}
init() {
let appData = AppData()
_appData = StateObject(wrappedValue: appData)
}
}
enum MainTab: String {
case location
case compass
case map
case items
case settings
}
enum LayersViewKind: String, CaseIterable, Codable {
case all
case visible
case recents
case deleted
var menuLabelTitle: String {
switch self {
case .all: return "All Maps"
case .visible: return "Visible"
case .recents: return "Recents"
case .deleted: return "Recently Deleted"
}
}
var color: Color {
switch self {
case .all: return .blue
case .visible: return .green
case .recents: return .orange
case .deleted: return .gray
}
}
var imageName: String {
switch self {
case .all: return "square.fill.on.square.fill"
case .visible: return "eye.fill"
case .recents: return "clock.fill"
case .deleted: return "trash.fill"
}
}
}
class SceneState: ObservableObject {
@Published var activeTab = MainTab.map
@Published var mapTabPath: [MapTabRoute] = []
@Published var itemsTabPath: [ItemsTabRoute] = []
var activeLayerID: UUID? {
switch mapTabPath.last {
case let .map(layerID):
return layerID
default:
return nil
}
}
}
struct MainScene: View {
@SceneStorage("MainScene.activeTab") var activeTab = MainTab.map
@SceneStorage("MainScene.mapTabPathData") var mapTabPathData: Data?
@SceneStorage("MainScene.itemsTabPathData") var itemsTabPathData: Data?
@StateObject private var sceneState = SceneState()
var body: some View {
MainView()
.onAppear {
Task { @MainActor in
sceneState.activeTab = activeTab
sceneState.mapTabPath = NavigationStore<MapTabRoute>.restore(data: mapTabPathData)
sceneState.itemsTabPath = NavigationStore<ItemsTabRoute>.restore(data: itemsTabPathData)
}
}
.onChange(of: sceneState.activeTab) { _ in
activeTab = sceneState.activeTab
}
.onChange(of: sceneState.mapTabPath) { _ in
mapTabPathData = NavigationStore<MapTabRoute>.save(path: sceneState.mapTabPath)
}
.onChange(of: sceneState.itemsTabPath) { _ in
itemsTabPathData = NavigationStore<ItemsTabRoute>.save(path: sceneState.itemsTabPath)
}
.environmentObject(sceneState)
}
}
struct MainView: View {
@EnvironmentObject var sceneState: SceneState
var body: some View {
TabView(selection: $sceneState.activeTab) {
Text("Location Tab")
.tag(MainTab.location)
.tabItem { Label("Location", systemImage: "location") }
CompassTab()
.tag(MainTab.compass)
.tabItem { Label("Compass", systemImage: "arrow.up.circle") }
MapTab()
.tag(MainTab.map)
.tabItem { Label("Map", systemImage: "map") }
if let activeLayerID = sceneState.activeLayerID {
ItemsTab(layerID: activeLayerID)
.tag(MainTab.items)
.tabItem { Label(activeLayerID.uuidString.prefix(3), systemImage: "folder") }
}
Text("Settings Tab")
.tag(MainTab.settings)
.tabItem { Label("Settings", systemImage: "gear") }
}
}
}
struct CompassTab: View {
@EnvironmentObject var sceneState: SceneState
var body: some View {
VStack {
Text("Compass Tab")
Button("Show Map") {
sceneState.activeTab = .map
}
.buttonStyle(.borderedProminent)
}
}
}
struct TopLevelFoldersView: View {
var body: some View {
List {
ForEach(LayersViewKind.allCases, id: \.self) { kind in
NavigationLink(value: MapTabRoute.layers(kind)) {
Label(kind.menuLabelTitle, systemImage: kind.imageName)
.foregroundColor(kind.color)
}
}
}
.navigationTitle("Folders")
}
}
struct LayersView: View {
let kind: LayersViewKind
@EnvironmentObject var appData: AppData
var body: some View {
List {
ForEach(appData.layers) { layer in
NavigationLink(value: MapTabRoute.map(layer.id)) {
Text(layer.name)
}
}
}
.navigationTitle(kind.menuLabelTitle)
}
}
struct MapView: View {
let layerID: UUID
@EnvironmentObject var appData: AppData
var body: some View {
if let layer = appData.findLayer(layerID: layerID) {
MapContentView(layer: layer)
} else {
Text("Unknown Layer: id:\(layerID)")
}
}
}
struct MapContentView: View {
let layer: Layer
var body: some View {
VStack {
Text(layer.name).font(.title)
Text(layer.id.uuidString.prefix(8)).monospaced()
}
.navigationTitle(layer.name)
.navigationBarTitleDisplayMode(.inline)
}
}
enum MapTabRoute: Hashable, Codable {
case layers(LayersViewKind)
case map(UUID)
}
enum ItemsTabRoute: Hashable, Codable {
case item(UUID)
}
struct MapTab: View {
@EnvironmentObject var sceneState: SceneState
@State private var readyToShow = false
var body: some View {
NavigationStack(path: $sceneState.mapTabPath) {
Group {
TopLevelFoldersView()
}
.navigationTitle("Folders")
.navigationDestination(for: MapTabRoute.self) { route in
switch route {
case let .layers(kind):
LayersView(kind: kind)
case let .map(layerID):
MapView(layerID: layerID)
}
}
}
}
}
struct ItemView: View {
let itemID: UUID
@EnvironmentObject var appData: AppData
var body: some View {
if let item = appData.findItem(itemID: itemID) {
ItemContentView(item: item)
} else {
Text("Unknown Item: id:\(itemID)")
}
}
}
struct ItemContentView: View {
@EnvironmentObject var sceneState: SceneState
let item: Item
var body: some View {
VStack {
Text(item.name).font(.title)
Text(item.id.uuidString.prefix(8)).monospaced()
Button("Show on Map") {
sceneState.activeTab = .map
}
.buttonStyle(.borderedProminent)
}
.navigationTitle(item.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct ItemsView: View {
let layerID: UUID
@EnvironmentObject var appData: AppData
var body: some View {
if let layer = appData.findLayer(layerID: layerID) {
ItemsContentView(layer: layer, items: appData.items)
} else {
Text("Unknown layerID: \(layerID)")
}
}
}
struct ItemsContentView: View {
let layer: Layer
let items: [Item]
var body: some View {
List {
ForEach(items) { item in
NavigationLink(value: ItemsTabRoute.item(item.id)) {
Text(item.name)
}
}
}
.navigationTitle(layer.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct ItemsTab: View {
let layerID: UUID
@EnvironmentObject var sceneState: SceneState
var body: some View {
NavigationStack(path: $sceneState.itemsTabPath) {
ItemsView(
layerID: layerID
)
.navigationDestination(for: ItemsTabRoute.self) { route in
switch route {
case let .item(itemID):
ItemView(itemID: itemID)
}
}
}
}
}
There appears to be a known issue with SwiftUI Charts not updating properly when used within a TabView, particularly in iOS 17.4.
The issue occurs when multiple Chart views are generated inside a ForEach loop within a TabView. Some charts may display incorrect data that doesn't match the corresponding tab.
Tapping or dragging on the chart can sometimes trigger it to update and display the correct data
Workaround is to use the .id()
modifier directly on the Chart object.
Chart {
// Chart content
}
.id(date)
Reference: https://www.perplexity.ai/search/multiple-swiftui-charts-in-a-t-v4Y3ck2eRdiwn_Lh.w0VIw
While working on Land Nav I stumbled across a view modifier called luminanceToAlpha
which when paired with .background(.green)
caused the map to have a cool night vision compatible look.
Unfortunately because I also want to switch that mode off and that luminanceToAlpha
isn't compatible with a ternary operator it will cause the expensive Mapbox view to be recreated each time that changes. This caused a 200MB memory leak which I wasn't able to determine why.
The result is that while it looks cool and I was hoping to go this direction I reverted the changed and stuck with the Dark Mapbox style so as to avoid the performance hit.
Not having conditional view modifiers in SwiftUI is a huge problem that feels easy to fix but the fix is almost certainly broken. See the reference for details..
Reference: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/
Problem: Current method of using notes is unstructured and hard to follow and would be difficult to share.
Solution: Use GitHub Issues to organize notes.
The idea was inspired by onmyway133 aka Khoa.
Today I encountered a difficult to understand crash when one ListView was pushing to another View (in this case another ListView but I don't that is related). In the detail view an action was performed which would cause a Section to be removed from the Parent view on next render. SwiftUI really did not like this and crashed are at @main
with an unhelpful stack trace.
Only through trial and error and vague recollection of encountering this before did I fix this by ensuring no sections are added or removed by child views of a ListView.
To see what is causing a view to update place this inside a var body
property:
var body: some View {
let _ = Self._printChanges()
}
References:
I've got a minimal Swift UI example that reproduces an odd warning that is happening by many people that I can't explain.
Using MapMarker removes the problem but when using MapAnnotation
as soon as the map is panned the app produces reams of Publishing changes from within view updates is not allowed, this will cause undefined behavior.
warnings in the output log.
Apple Developer Forums discussion doesn't have any solutions.
https://developer.apple.com/forums/thread/718697
import MapKit
import SwiftUI
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Observation: Identifiable {
let id: String
let coordinate: CLLocationCoordinate2D
}
struct ContentView: View {
@State var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 49.3, longitude: -123.2),
latitudinalMeters: 20_000,
longitudinalMeters: 20_000
)
let observations: [Observation] = [
.init(id: "1", coordinate: CLLocationCoordinate2D(latitude: 49.3, longitude: -123.2))
]
var body: some View {
Map(
coordinateRegion: $region,
annotationItems: observations,
annotationContent: { observation in
// MapMarker(coordinate: observation.coordinate)
MapAnnotation(coordinate: observation.coordinate) {
// Circle().fill(.red).frame(width: 10, height: 10)
EmptyView()
}
}
)
}
}
Apple support document keeps an updated list:
https://support.apple.com/en-us/HT206175
Found via NSHipster article
Add "App Uses Non-Exempt Encryption" to your Info.plist
. After this App Store Connect won't ask if you are is using cryptography every time you upload a build to TestFlight ๐
Source: https://twitter.com/KSlazinski/status/1573980451550613504?s=20
import SwiftUI
struct SizeClassView: View {
@Environment(\.horizontalSizeClass) var hor: UserInterfaceSizeClass?
@Environment(\.verticalSizeClass) var ver: UserInterfaceSizeClass?
var body: some View {
VStack(alignment: .center, spacing: 10) {
Text("horizontal size class \(hor == .regular ? "regular" : "compact")")
Text("vertical size class \(ver == .regular ? "regular" : "compact")")
if hor == .regular {
Text("You can see me, if there is enough space.")
}
}
}
}
compact
width and regular
height.compact
width and compact
height.regular
width and compact
height.regular
width and regular
height when full screen.References:
Noticed on a person's phone the iOS and Land Nav's compass not giving heading readings. If I turned off use True North in the built in compass app headings would work.
The problem was here...
Privacy & Security > Location Services > System Services > Compass Calibration was toggled off.
Reference: https://discussions.apple.com/thread/6776025
I can't seem to figure out what SwiftUI needs to make animating rows into different sections work.
I'm trying to build a simple view that allows "pinning" items in a list.
I've built a simple app that attempts to implement this but it fails in various ways.
.onTapGesture
to toggle pin state animates the list well in all cases. Unfortunately this isn't the UX I want to use..swipeActions
causes glitches when the item is the first to be pinned. Once the pinned section is in the view tree pinning other rows works well. Unpinning works well until it will hide the section again..contextMenu
works when it is the first item to be pinned but glitches when pinning additional items (oppose of .swipeActions
WTF?). This can be hacked by putting an artificial delay of around 500-700ms before toggling the state.My preferred way to capture and view emails being sent by a Rails app in development is to use Mailpit. It runs a local SMTP server at port 1025
and a barebones email client at port 8025
. I used to use MailHog but development has stalled. Mailpit has the same ease of use while supporting newer features and looking nicer.
The mailpit
utility can be installed with brew
:
brew install mailpit
brew services start mailpit
The development smtp
settings are configured in config/environments/development.rb
:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { address: "localhost", port: 1025 }
All outgoing email from the development server will be captured and viewable in
both html
and text
form at localhost:8025
.
Source: https://github.com/jbranchaud/til/blob/master/rails/capture-development-emails-with-mailhog.md
There are a couple of simple structs that are easily made Codable. I want to save in AppDefaults and I thought I would be able to mark them as RawRepresentable and using JSON to encode/decode the rawValue.
Turns out this leads to an infinite recursion and eventual crash.
The solution is to provide a specific Codable implementation.
Reference:
Apps need to provide a Share Extension if they want to appear on the Activity Sheet when a user shares content from that app.
E.g., when sharing a location from the Maps app you will see the Lyft app available. This lets you share a location in Maps as a destination in Lyft.
It is a 2 step process.
References:
I'm trying to make a ListView have an attractive top level menu of different folders represented by an icon and text.
Because each image is slightly different sizes, the eye is wide and the trash icon is tall this requires drawing all the images on each row and hiding all but the visible one in a ZStack to get the layout correct.
enum LayersViewKind: CaseIterable {
case all
case visible
case recents
case deleted
var menuLabelTitle: String {
switch self {
case .all: return "All Layers"
case .visible: return "Visible"
case .recents: return "Recents"
case .deleted: return "Recently Deleted"
}
}
var color: Color {
switch self {
case .all: return .blue
case .visible: return .green
case .recents: return .orange
case .deleted: return .gray
}
}
var imageName: String {
switch self {
case .all: return "square.fill.on.square.fill"
case .visible: return "eye.fill"
case .recents: return "clock.fill"
case .deleted: return "trash.fill"
}
}
}
struct HomeMenuLabel: View {
let kind: LayersViewKind
var body: some View {
HStack {
// Stack all images for each row to insure icon and text is centered. If you know of a better way
ZStack {
ForEach(LayersViewKind.allCases, id: \.self) { currentKind in
Image(systemName: currentKind.imageName)
.imageScale(.medium)
.padding(MagicValue.mediumPadAmount)
.foregroundColor(.white)
.background(kind.color)
.clipShape(Circle())
.opacity(kind.imageName == currentKind.imageName ? 1 : 0)
.accessibilityHidden(kind.imageName != currentKind.imageName)
}
}
Text(kind.menuLabelTitle)
}
}
}
There has to be a better way to do this but I can't think of what it would be. Maybe GeometryReader but then would that be any less complicated?
Environment: Xcode 14.2 Swift iOS 16.2 iPhone 12 mini. Must run on device, simulator works as expected.
The menu to close and the sheet to open.
The menu closes as expected but the sheet does not open even though the state is changed by the button. If the menu is not open this does not happen.
import SwiftUI
@main
struct SheetMenuBugApp: App {
@State private var showSheet = false
var body: some Scene {
WindowGroup {
VStack {
Button("Open Sheet") { showSheet = true }
Text("showSheet: \(showSheet ? "T" : "F")")
Spacer()
Text("Open Sheet doesn't work if you show the menu and tap Open Sheet.")
Spacer()
Menu(
content: { Text("Empty Menu") },
label: { Text("Show Menu") }
)
}
.padding()
.sheet(isPresented: $showSheet) { Text("Sheet Contents") }
}
}
}
wikibase_item
value. This will be a string starting with Q, e.g., Q26733
.P141
.Q
string followed by a number. E.g., Q211005. You can look up details for any property using the WikiData, .e.g, https://www.wikidata.org/wiki/Q211005If you want a strong vibration the original vibrate sound effect works well using
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
But it won't play if the device is silent mode.
The new AVAudioPlayer APIs allow you to specify the app category so you can explicitly state that you are app is using audio in a playback category to play audio while the device is in silent mode. This is great except that there is no option to use the old kSystemSoundID_Vibrate
effect.
Install with Homebrew
brew install codespell
Example how to skip specific folders and files ignoring lines that found in the file .codespellignorelines
. The ignore line file is codespell's way of silencing false positives that can creep up.
codespell --enable-colors --exclude-file .codespellignorelines --skip deps,priv,erl_crash.dump
I discovered this tool when I noticed in the Rails CI lint workflow.
Source:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.