See sample code below...
Basically it's a galleryView with a dataSource that can add/remove items dynamically. It works as expected when GalleryView's dataSource variable has a type (that conforms to ObservableObject)
However when I change dataSource's type to be a protocol, I can't seem to get my code to compile.
Any guidance on how to use a protocol in GalleryView, and continue to keep the UI updating when the model object's item list changes?
thanks!
Mike
protocol GalleryDataSource: ObservableObject {
var itemCount: Int { get }
func item(for index: Int) - String
}
class GalleryModel: ObservableObject {
static let test1: GalleryModel = GalleryModel(items: ["A","B","C"])
@Published var items: [String]
init(items: [String]) {
self.items = items
}
}
extension GalleryModel: GalleryDataSource {
var itemCount: Int {
return items.count
}
func item(for index: Int) - String {
return items[index]
}
}
struct ContentView: View {
var model: GalleryModel = GalleryModel.test1
var body: some View {
VStack {
GalleryView(dataSource: model)
Button("Add Item") {
model.items.append("\(model.items.count)")
}
}
}
}
struct GalleryView: View {
@ObservedObject var dataSource: GalleryModel //GalleryDataSource
var body: some View {
ScrollView(.horizontal, content: {
HStack {
ForEach(0..self.dataSource.itemCount, id:\.self) { index in
Text(dataSource.item(for: index))
.padding()
}
}
})
}
}
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I'm trying to find a syntactically correct way to put the contents of a Container in a separate variable (or function).
Can anyone steer me in the right direction?
thanks, in advance,
mike
struct ContentView: View {
var body: some View {
VStack(content: containerContent)
.padding()
}
var containerContent: () -> Content {
return {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
}
}
I have a SwiftUI view that works as expected in a full SwiftUI context. But I now need to use it in a UIViewController. It mostly works, but I'm trying to expose an @State var out to the viewController, and it only ever returns the initial value.
Any guidance for how best to pass out this @State var?
I could make it be a binding, but in my pure SwiftUI code, it works fine as @State (ie EditorView's container view does not need to know about sliderVal)
thanks, in advance, for any suggestions
import SwiftUI
import UIKit
class ViewController: UIViewController {
var host: UIHostingController<EditorView>?
override func viewDidLoad() {
super.viewDidLoad()
host = .init(rootView: EditorView())
guard let host = host else { return }
addChild(host)
host.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(host.view)
host.didMove(toParent: self)
}
@IBAction func helloTapped(sender: UIButton) {
guard let sliderValue = host?.rootView.sliderVal else { return }
print("UIKit sliderValue: \(sliderValue)")
}
}
struct EditorView: View {
@State var sliderVal: Double
init(sliderVal: Double? = nil) {
_sliderVal = State(initialValue: sliderVal ?? 7)
}
var body: some View {
VStack {
Slider(value: $sliderVal, in: 1...10)
Text("sliderVal: \(sliderVal)")
}
}
}
(NOTE: in order to see this code snippet in action you will need to create a button in the storyboard and link it to helloTapped)
This first code works fine decoding json.
static let localizationsDictText = """
{ "en" : { "string" : "Cat" },
"fr" : { "string" : "Chat"}
}
"""
struct Localization: Codable, Equatable {
let string: String
}
typealias LocalizationsDict = [String: Localization]
func testExample() throws {
let text = Self.localizationsDictText
let data = text.data(using: .utf8, allowLossyConversion: false)
let localizationsDict = try JSONDecoder().decode(LocalizationsDict.self, from: data!)
XCTAssertEqual(localizationsDict.keys.count, 2)
}
But then I try to create a modified version of the above, only changing the type of the LocalizationsDict key from String to an enum:
enum Language: String, CaseIterable, Codable {
case en = "en"
case fr = "fr"
}
typealias LocalizationsDict2 = [Language: Localization]
func testExample2() throws {
let text = Self.localizationsDictText
let data = text.data(using: .utf8, allowLossyConversion: false)
let localizationsDict = try JSONDecoder().decode(LocalizationsDict2.self, from: data!)
XCTAssertEqual(localizationsDict.keys.count, 2)
}
and the JSONDecoder line throws an exception:
testExample2(): failed: caught error: "typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))"
Any suggestions as to why switching from
[String: Localization] to [Language: Localization] might cause JSONDecode() to treat it like an array?
Thanks in advance for any guidance.
I am using @AppStorage in a model object (see code below that works as expected).
class ModelObject {
static let shared = ModelObject()
@AppStorage("enhanced") var scriptPickers: Bool = true
var defaultDependentValue: String {
scriptPickers ? "Enhanced" : "NOT enhanced"
}
}
struct ContentView: View {
@AppStorage("enhanced") var scriptPickers: Bool = true
var body: some View {
VStack {
Toggle(isOn: $scriptPickers, label: {
Text("userDefault val")
})
Text("value: \(ModelObject.shared.defaultDependentValue)")
}
}
}
Now I want to test my model object in a way that will allow me to use a mock instance of UserDefaults, but am having trouble with the syntax. I tried adding a userDefaults var, and referring to the var in the @AppStorage
class ModelObject {
static let shared = ModelObject()
let userDefaults: UserDefaults
init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
@AppStorage("enhanced", store: userDefaults) var scriptPickers: Bool = true
var defaultDependentValue: String {
scriptPickers ? "Enhanced" : "NOT enhanced"
}
}
However I can't find a way to avoid the syntax error this generates:
Cannot use instance member 'userDefaults' within property initializer; property initializers run before 'self' is available
Any guidance on how I might be able to:
continue using @AppStorage
be able to test my class in a way that doesn't force me to use UserDefaults.standard
thanks, in advance,
Mike
In V 0.1 of my app, I went with a simple option for a piece of business logic in my app.
I then changed something else that caused my simplified business logic to now crash the app at startup.
Excellent, a chance to use Test. Driven Design to replace my flawed business logic.
So I wrote my first test TDD test, but when I tried to run the test... It doesn't run because running the test target seems to start by actually running my full app (which as I described in chapter 1 is currently crashing)
Have I configured something incorrectly? or is it normal that when I attempt to run a single test, step 1 is running the full app?
(note: it is very easy for me to disable/avoid the business logic causing my crash. This question is more about my lack of understanding of what it means to run a test target.)
thanks in advance for any and all responses.
Mike
I've defined a value stored in UserDefaults.
In a view struct I have code that can successfully update the stored value.
I've also added an @AppStorage var in an instance of a class, that can read this value and run business logic that depends on the current stored value.
But what I really want to do, is have code in my class that gets automatically called when the value stored in UserDefaults gets updated.
Basically I want to do this:
@AppStorage("languageChoice") var languageChoice: LanguageChoice = .all {
didSet {
print("hello")
}
}
Unfortunately didSet closures in @AppStorage vars do not appear to get called :-(
My clumsy attempts to use combine have all ended in tears from the compiler.
Any/all suggestions are greatly appreciated.
thanks,
Mike
From what I've read, @AppStorage vars should be @Published, however the following code generates a syntax error at extended's .sink modifier: Cannot call value of non-function type 'Binding<Subject>'
class LanguageManager: ObservableObject {
@Published var fred = "Fred"
@AppStorage("extended") var extended: Bool = true
private var subscriptions = Set<AnyCancellable>()
init() {
$fred
.sink(receiveValue: {value in
print("value: \(value)")
})
.store(in: &subscriptions)
$extended
.sink(receiveValue: {value in
print("value: \(value)")
})
.store(in: &subscriptions)
}
Does anyone know of a way to listen for (subscribe to) changes in @AppStorage values?
didSet works in for a specific subset of value changes, but this is not sufficient for my intended use.
I thought the following code would allow me to have focus in the TextField when the app loads. Is there something else/different that I need to do?
struct ContentView: View {
enum FocusField {
case password
}
@State var fieldContent: String = ""
@FocusState var focus: FocusField?
var body: some View {
VStack {
TextField("Enter text here", text: $fieldContent)
.focused($focus, equals: .password)
Text("Hello, world!")
}
.padding()
.defaultFocus($focus, .password)
}
}
I have a CoreImage pipeline and one of my steps is to rotate my image about the origin (bottom left corner) and then translate it. I'm not seeing the behaviour I'm expecting, and I think my problem is in how I'm combining these two steps.
As an example, I start with an identity transform
(lldb) po transform333
▿ CGAffineTransform
- a : 1.0
- b : 0.0
- c : 0.0
- d : 1.0
- tx : 0.0
- ty : 0.0
I then rotate 1.57 radians (approx. 90 degrees, CCW)
transform333 = transform333.rotated(by: 1.57)
- a : 0.0007963267107332633
- b : 0.9999996829318346
- c : -0.9999996829318346
- d : 0.0007963267107332633
- tx : 0.0
- ty : 0.0
I understand the current contents of the transform.
But then I translate by 10, 10:
(lldb) po transform333.translatedBy(x: 10, y: 10)
- a : 0.0007963267107332633
- b : 0.9999996829318346
- c : -0.9999996829318346
- d : 0.0007963267107332633
- tx : -9.992033562211013
- ty : 10.007960096425679
I was expecting tx and ty to be 10 and 10.
I have noticed that when I reverse the order of these operations, the transform contents look correct. So I'll most likely just perform the steps in what feels to me like the incorrect order.
Is anyone willing/able to point me to an explanation of why the steps I'm performing are giving me these results?
thanks,
mike
For my app I've created a Dictionary that I want to persist using AppStorage
In order to be able to do this, I added RawRepresentable conformance for my specific type of Dictionary. (see code below)
typealias ScriptPickers = [Language: Bool]
extension ScriptPickers: @retroactive RawRepresentable where Key == Language, Value == Bool {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(ScriptPickers.self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self), // data is Data type
let result = String(data: data, encoding: .utf8) // coerce NSData to String
else {
return "{}" // empty Dictionary represented as String
}
return result
}
}
public enum Language: String, Codable, {
case en = "en"
case fr = "fr"
case ja = "ja"
case ko = "ko"
case hr = "hr"
case de = "de"
}
This all works fine in my app, however trying to run any tests, the build fails with the following:
Conflicting conformance of 'Dictionary<Key, Value>' to protocol 'RawRepresentable'; there cannot be more than one conformance, even with different conditional bounds
But then when I comment out my RawRepresentable implementation, I get the following error when attempting to run tests:
Value of type 'ScriptPickers' (aka 'Dictionary<Language, Bool>') has no member 'rawValue'
I hope Joseph Heller is out there somewhere chuckling at my predicament
any/all ideas greatly appreciated
In my code, I do this:
Text("\(languagesManager.availableWords.count)")
And next time I build, this creates an entry in Localizable.strings: %lld
Is there a way I can flag this UI element to indicate its string doesn't need to be localized?
It's related to the passByValue nature of structs. In the sample code below, I'm displaying a list of structs (and I can add instances to my list using Int.random(1..<3) to pick one of two possible predefined versions of the struct).
I also have a detail view that can modify the details of a single struct. However when I run this code, it will instead modify all the instances (ie either Sunday or Monday) in my list.
To see this behaviour, run the following code and:
tap New Trigger enough times that there are multiple of at least one of the sunday/monday triggers
tap one of the matching trigger rows
modify either the day, or the int
expected: only one of the rows will reflect the edit
actual: all the matching instances will be updated.
This suggests to me that my Sunday and Monday static instances are being passed by reference when they get added to the array. But I had thought structs were strictly pass by value. What am I missing?
thanks in advance for any wisdom,
Mike
struct ContentView: View {
@State var fetchTriggers: [FetchTrigger] = []
var body: some View {
NavigationView {
VStack {
Button("New Trigger") {
fetchTriggers.append(Int.random(in: 1..<3) == 1 ? .sunMorning : .monEvening)
}
List($fetchTriggers) { fetchTrigger in
NavigationLink(destination: FetchTriggerDetailView(fetchTrigger: fetchTrigger)
.navigationBarTitle("Back", displayMode: .inline))
{
Text(fetchTrigger.wrappedValue.description)
.padding()
}
}
}
}
}
}
struct FetchTrigger: Identifiable {
static let monEvening: FetchTrigger = .init(dayOfWeek: .monday, hour: 6)
static let sunMorning: FetchTrigger = .init(dayOfWeek: .sunday, hour: 3)
let id = UUID()
enum DayOfWeek: Int, Codable, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case sunday = 1
case monday
case tuesday
var description: String {
switch self {
case .sunday: return "Sunday"
case .monday: return "Monday"
case .tuesday: return "Tuesday"
}
}
}
var dayOfWeek: DayOfWeek
var hour: Int
var description: String {
"\(dayOfWeek.description), \(hour):00"
}
}
struct FetchTriggerDetailView: View {
@Binding var fetchTrigger: FetchTrigger
var body: some View {
HStack {
Picker("", selection: $fetchTrigger.dayOfWeek) {
ForEach(FetchTrigger.DayOfWeek.allCases) { dayOfWeek in
Text(dayOfWeek.description)
.tag(dayOfWeek)
}
}
Picker("", selection: $fetchTrigger.hour) {
ForEach(1...12, id: \.self) { number in
Text("\(number)")
.tag(number)
}
}
}
}
}
When I run this in a playground:
var meDate = Calendar.current.date(from: DateComponents(year: 2024, hour: 7, weekday: 3, weekdayOrdinal: 2))!
print(meDate)
I see:
2024-01-09 15:00:00 +0000
This seems correct to me.
jan 9th is the second Tuesday in 2024
I'm in the pacific TZ, 07:00 PDT matches 15:00GMT
But then I do this:
meDate = Calendar.current.date(bySetting: .weekday, value: 4, of: meDate)!
print(meDate)
and I see: 2024-01-10 08:00:00 +0000
I would have expected my hour value (7PST/15GMT) to have been preserved. Is there a way I can update weekday, but not lose my hour?
Hello folks,
I'm attempting to implement some swiftUI UI code to support filtering of a list.
One part of the filtering involves displaying one checkbox for each case/value of an enum (TangleType below)
TangleFilter is a model class that includes an array of TangleTypeFilter objects (each owning a single bool value and a binding)
Expected behaviour: when user taps a checkbox, the checkbox toggles the display and the filter model object toggles its value.
Actual behaviour: the model is updating appropriately, however the UI is not updating. (the single filter below the list does in fact behave correctly
any and all guidance greatly appreciated
Mike
struct ContentView: View {
@State var isChecked: Bool = false
@ObservedObject var filter = TangleFilter()
@ObservedObject var singleFilter: TangleTypeFilter
init() {
self.singleFilter = TangleTypeFilter(tangleType: .grid)
}
var body: some View {
VStack{
List(filter.tangleTypes, id: \.self) {tangleTypeFilter in
HStack {
// when uncommented the following line returns the following
// compile error:
// Use of unresolved identifier '$tangleTypeFilter'
// CheckBox(isChecked: $tangleTypeFilter.isChecked)
CheckBox(isChecked: tangleTypeFilter.binding)
Text("checked? \(tangleTypeFilter.isChecked.description)")
}
}
CheckBox(isChecked: $singleFilter.isChecked)
}
}
}
struct CheckBox: View {
@Binding var isChecked: Bool {
didSet {
print("setting isChecked: \(isChecked)")
}
}
var imageName: String {
return isChecked ? "checkmark.square" : "square"
}
var body: some View {
Button(action: {
self.isChecked.toggle()
}) {
Image(systemName: self.imageName)
}
}
}
enum TangleType: String, Codable, CaseIterable {
static let filterArray: [TangleTypeFilter] = {
var result: [TangleTypeFilter] = []
for tangleType in TangleType.allCases {
result.append(TangleTypeFilter(tangleType: tangleType))
}
return result
}()
case grid
case row
}
class TangleFilter: ObservableObject {
@Published var tangleTypes: [TangleTypeFilter] = TangleType.filterArray
}
class TangleTypeFilter: ObservableObject {
var tangleType: TangleType
@Published var isChecked: Bool
lazy var binding: Binding<Bool> = Binding(get: {
return self.isChecked
}, set: {
self.isChecked = $0
})
init(tangleType: TangleType) {
self.tangleType = tangleType
self.isChecked = false
}
}
extension TangleTypeFilter: Hashable {
static func == (lhs: TangleTypeFilter, rhs: TangleTypeFilter) -> Bool {
return lhs.tangleType == rhs.tangleType
}
func hash(into hasher: inout Hasher) {
hasher.combine(tangleType)
}
}