SwiftUI - Wrong index with removeAll(where: ) - swiftui

I want to let the user remove an image in an array. When the image is pressed, an action sheet is presented so the user can confirm the removal of the image. The problem is that it removes the wrong image. It always removes the first image of the array.
#State var pickerResult: [SImage] = []
...
ScrollView(.horizontal, showsIndicators: true){
HStack{
ForEach(pickerResult) { simage in
Image(uiImage: simage.image)
.onTapGesture() {
imageActionSheetIsPresented = true
// This will work: self.pickerResult.removeAll(where: {$0.image == simage.image})
}
.actionSheet(isPresented: $imageActionSheetIsPresented) {
ActionSheet(title: Text("Do you want to remove the image?"), buttons: [
.default(Text("Remove image")){
self.pickerResult.removeAll(where: {$0.image == simage.image})
print(simage.id)
// Returns id of the first image in the array
},
.cancel()
])
}
}
}
}
As you see in the code, skipping the confirmation and letting the user remove the image with onTap will work just fine.
Some how 'simage' is always the first item in the array when using ActionSheet.
Here is the SImage:
struct SImage: Identifiable{
var id = UUID()
var image: UIImage
}

Related

SwiftUI - Picker has two sets of init - what are the second set for? and how do i get selected item?

Im going through SwiftUI Controls in detail and writing example code for each init.
Theres two sets of inits for Picker.
One where you fill your list from an array etc. and what you pick goes into selection: binding.
But whats the second set of inits for?
Theyre under section 'Creating a picker for a collection'
I can fill the array from a collection using both these inits ok
But how do you get the selected item using the 2nd set of inits?
The selection: param is no longer a binding to an ivar but a Keypath to fill the list.
My question is how do I get the selected item using the 2nd set of inits.
See inits here:
https://developer.apple.com/documentation/swiftui/picker
For Picker there are 6 inits.
3 under 'Creating a picker'
these are ok. I fill the list from an array for example and store the selected item in a single result specified by the selection: param. It binds the result to one ivar.
There are also 3 inits under 'Creating a picker for a collection'
I got this to display the items from a collection
e.g. I modified the example code in the apple docs. The code in the docs doesn't compile so apple may be missing stuff.
import SwiftUI
enum Thickness: String, CaseIterable, Identifiable {
case thin
case regular
case thick
var id: String { rawValue }
}
//to use in ist must be Hashable
struct Border: Identifiable {
var color: Color
var thickness: Thickness
//Identifiable > Hashable > id > String
//var id: String { return "\(color.hashValue)" }
let id = UUID()
}
extension Color{
func colorName() -> String{
if self == Color.black{
return "black"
}
else if self == Color.red{
return "red"
}
else{
return "UNHANDLED"
}
}
}
struct CLCPickers_selection_FromCollection_View: View {
#State private var selectedObjectBorders = [
Border(color: .black, thickness: .thin),
Border(color: .red, thickness: .thick)
]
var body: some View {
VStack{
//------------------------------------------------------------------
Picker(
"Border Thickness",
sources: $selectedObjectBorders,
selection: \.thickness
) {
//------------------------------------------------------------------
//I added
//id: \.self
//Picker: the selection "thin" is invalid and does not have an associated tag, this will give undefined results.
//------------------------------------------------------------------
ForEach(Thickness.allCases,
id: \.self)
{ thickness in
Text(thickness.rawValue)
}
}
//------------------------------------------------------------------
Divider()
//------------------------------------------------------------------
//This just lists the colors in the arrays of Border
//QUESTION - how do I find out the currenly selected one?
//normaly selection: in the picker would be bound to the picked item
//but for this init selection: is a keypath
//selection: \.thickness
//so I can fill the Picker list using the keypath into the Border array.
//BUT HOW DO I FIND OUT THE CURRENTLY SELECTED ITEM?
//theres no binding?
//is there a .selectedItem property some where?
List(selectedObjectBorders) {
Text("\($0.color.colorName())")
}
}
}
}
Question was answered but poster removed it for some reason.
answer: this picker init which set the thinkness ivar of EVERY Border object in the collection.
To see the change I should have displayed the result to show thickness.rawvalue to see the change in every Border object
List(selectedObjectBorders) { border in
HStack{
Text("\(border.color.colorName())")
Text("\(border.thickness.rawValue)") //<<- will change when you select an item. All will match.
}
}

Vertical SwiftUIPager breaks when adding content on the start of the list, on demand

I have an app, that shows posts on a vertical SwiftUIPager. Each page fills the whole screen.
So, when the app launches, I fetch the 10 most recent posts and display them.
As soon as those posts are fetched, I start listening for new posts. (see code below) Whenever that callback gets triggered, a new post is created. I take it and place it on the top of my list.
The thing is when I scroll to find the new post, its views get mixed up with the views of the next post.
Here's what I mean:
Before the new post, I have the one below
https://imgur.com/a/ZmMzfvb
And then, a new post is added to the top
https://imgur.com/a/PJ0trSF
As you'll notice the image seems to be the same, but it shouldn’t! If I scroll for a while and then go back up, the new post will be fixed and display the proper image. (I'm using SDWebImageSwiftUI for async images, but I don't think it matters... I also used Apple's AsyncImage, with the same results)
Here's my feed view model:
#Published var feedPage: Page = .first()
#Published var feedItems = Array(0..<2)
var posts = [Post]()
...
private func subscribeToNewPosts() {
postsService.subscribeToNewPosts() { [weak self] post in
self?.posts.insert(post, at: 0)
DispatchQueue.main.async {
self?.feedItems = Array(0..<(self?.posts.count ?? 1))
}
}
}
And here's my feed view:
private struct FeedPageView: View {
#EnvironmentObject private var viewModel: FeedView.ViewModel
var body: some View {
ZStack {
VStack {
Pager(page: viewModel.feedPage,
data: viewModel.feedItems,
id: \.self,
content: { index in
if index == 0 {
HomeCameraView()
.background(.black)
} else {
PostView(post: viewModel.posts[index - 1])
}
})
.vertical()
.sensitivity(.custom(0.1))
.onPageWillChange { index in
viewModel.willChangeVerticalPage(index: index)
}
}
}
}
}
Any idea what I'm doing wrong?

SwiftUI - NSFontPanel and Color Picker

I am trying to get NSFontPanel/NSFontManager to work in a SwiftUI Document Template app. I have the following which is a customize version of one I found on GitHub. This lets me pick the size, face, style, etc.
Interestingly, a color picker is included in the FontPanel. The documentation doesn't seem to say this. Is this something new?
Anyway, I would like to either be able to use the color picker to let the user select a color, or if not I would like to hide the color picker - at is not "critical" to this application. I am using this to allow customization of text in a sidebar, so color is nice, but not necessary. Currently the Font settings are working, but the color selection displays, and let you pick on, but it always returns System Color.
Any help would be appreciated.
NOTE: I didn't include the FontPickerDelegate, it just calls this:
public struct FontPicker: View{
let labelString: String
#Binding var font: NSFont
#State var fontPickerDelegate: FontPickerDelegate?
public init(_ label: String, selection: Binding<NSFont>) {
self.labelString = label
self._font = selection
}
let fontManager = NSFontManager.shared
let fontPanel = NSFontPanel.shared
#AppStorage("setSidebarFont") var setSidebarFont = "System"
#AppStorage("setSidebarFontSize") var setSidebarFontSize = 24
#AppStorage("setSidebarFontColor") var setSidebarFontColor = "gray"
public var body: some View {
HStack {
Text(labelString)
Button {
if fontPanel.isVisible {
fontPanel.orderOut(nil)
return
}
self.fontPickerDelegate = FontPickerDelegate(self)
fontManager.target = self.fontPickerDelegate
fontManager.action = #selector(fontPickerDelegate?.changeAttributes)
fontPanel.setPanelFont(self.font, isMultiple: false)
fontPanel.orderBack(nil)
} label: {
Text("Font Selection: \(setSidebarFont)")
.font(.custom(setSidebarFont, size: CGFloat(setSidebarFontSize)))
}
}
}
func fontSelected() {
self.font = fontPanel.convert(self.font)
setSidebarFont = self.font.displayName ?? "System"
setSidebarFontSize = Int(self.font.pointSize)
var newAttributes = fontManager.convertAttributes([String : AnyObject]())
newAttributes["NSForegroundColorAttributeName"] = newAttributes["NSColor"]
newAttributes["NSUnderlineStyleAttributeName"] = newAttributes["NSUnderline"]
newAttributes["NSStrikethroughStyleAttributeName"] = newAttributes["NSStrikethrough"]
newAttributes["NSUnderlineColorAttributeName"] = newAttributes["NSUnderlineColor"]
newAttributes["NSStrikethroughColorAttributeName"] = newAttributes["NSStrikethroughColor"]
print("\(newAttributes["NSForegroundColorAttributeName"]!)")
}
}

How to completely delete items when using #ObservedResults in Realm and SwiftUI

I'm trying the new Realm 10 wrappers by using the example given in the Realm documentation Integration Guides -> SwiftUI & Combine and I like how simple it is to add and delete records when using the #ObservedResults and the #ObservedRealmObject. The one thing I don't quite understand is why when deleting items from the Group object it only removes the items from the Group but leaves the actual items in the Item Realm object undeleted. See the Realm Browser image below.
Here is what the Realm Browser shows after adding and deleting four (4) items through the app UI, as you can see the four (4) items were deleted from the Group but left all four (4) items in the Item object.
Can someone please explain why the items don't get deleted from the Item object only from the Group object when calling .onDelete(perform: $group.items.remove)? How can I delete them?
I tried deleting them like this...
ItemsView.swift
.onDelete(perform: deleteItems)
func deleteItems(at offsets: IndexSet){
let realm = try? Realm()
try! realm?.write {
// 1. delete items
for item in list.items{
realm?.delete(item)
}
// 2. delete the list
realm?.delete(list)
}
}
but I got the following error:
Thread 1: "Can only delete an object from the Realm it belongs to."
Again, the whole code can be found in the Integration Guides - Without Sync.
EDIT: Added code for, LocalOnlyContentView, ItemsView, Group and Item models.
Item.swift
import Foundation
import RealmSwift
/// Random adjectives for more interesting demo item names
let randomAdjectives = [
"fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden",
"acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen",
"aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet"
]
/// Random noun for more interesting demo item names
let randomNouns = [
"floor", "monitor", "hair tie", "puddle", "hair brush", "bread",
"cinder block", "glass", "ring", "twister", "coasters", "fridge",
"toe ring", "bracelet", "cabinet", "nail file", "plate", "lace",
"cork", "mouse pad"
]
final class Item: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
#Persisted var isFavorite = false
#Persisted(originProperty: "items") var group: LinkingObjects<Group>
}
Group.swift
import Foundation
import RealmSwift
final class Group: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var items = RealmSwift.List<Item>()
}
ItemsView.swift
struct ItemsView: View {
#ObservedRealmObject var group: Group
var leadingBarButton: AnyView?
var body: some View {
NavigationView {
VStack {
// The list shows the items in the realm.
List {
ForEach(group.items) { item in
ItemRow(item: item)
}
.onDelete(perform: $group.items.remove)
.onMove(perform: $group.items.move)
}.listStyle(GroupedListStyle())
.navigationBarTitle("Items", displayMode: .large)
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: self.leadingBarButton,
trailing: EditButton())
HStack {
Spacer()
Button(action: {
$group.items.append(Item())
}) { Image(systemName: "plus") }
}.padding()
}
}
}
}
LocalOnlyContentView.swift
struct LocalOnlyContentView: View {
#ObservedResults(Group.self) var groups
var body: some View {
if let group = groups.first {
AnyView(ItemsView(group: group))
} else {
AnyView(ProgressView().onAppear {
$groups.append(Group())
})
}
}
}
SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = LocalOnlyContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Keep in mind that what's being shown in that tutorial is how to remove an item from a group's List - not how to totally delete the item.
Going very high level with this answer - objects in a List are not the actual objects - they are a "pointer" to the actual item stored on disk.
Suppose you have three items
Item 0
Item 1
Item 2
and a Group with List of items
MyGroup
List of items
Item 0
Item 1
Item 2
What's actually going on is the List "points" to the items on disk
My Group
List of items
pointer to Item 0
pointer to Item 1
pointer to Item 2
So when this is called $group.items.remove it's removing the pointer to the item from the list, not the item itself.
The solution (well, one of the solutions) is to remove the actual item
let myItemToRemove = List of items[0] //get the item at index 0
realm.remove(myItemToRemove) // removes the item itself from realm, along
// with the pointer stored in the list
The code to actually delete an item is this
try? realm.write {
realm.delete(objectToDelete)
}

Efficient way to model the data for SwiftUI

I am exploring SwiftUI+Combine with a demo app BP Management.
Homescreen has a provision to take bp readings(systolicBP, diastolicBP, pulse & weight).
Button "Next" is enabled only when all 4 fields are filled.
control should fall to the next textfield when a valid input is entered. (input is valid when it falls between the range specified by the placeholder - refer the image below)
On tapping next, on the detail screen user can edit the bp values (taken in the HomeScreen), additionally he can add recorded date, notes...
Thought enums would be best model this so I proceeded like
enum SBPInput: CaseIterable {
//name is a Text to indicate the specific row
typealias Field = (name: String, placeholder: String)
case spb, dbp, pulse, weight, note, date
var field: Field {
switch self {
case .dbp: return ("DBP", "40-250")
case .spb: return ("SBP", "50-300")
case .pulse: return ("Pulse", "40-400")
case .weight: return ("Weight", "30-350")
case .note: return ("Note", "")
case .date: return ("", Date().description)
}
}
// Here I am getting it wrong, - I can't bind a read only property
var value: CurrentValueSubject<String, Never> {
switch self {
case .date:
return CurrentValueSubject<String, Never>(Date().description)
case .spb:
return CurrentValueSubject<String, Never>("")
case .dbp:
return CurrentValueSubject<String, Never>("")
case .pulse:
return CurrentValueSubject<String, Never>("")
case .weight:
return CurrentValueSubject<String, Never>("70")
case .note:
return CurrentValueSubject<String, Never>("")
}
}
}
class HomeViewModel: ObservableObject {
#Published var aFieldsisEmpty: Bool = true
var cancellable: AnyCancellable?
var dataSoure = BPInput.allCases
init() {
var bpPublishers = (0...3).map{ BPInput.allCases[$0].value }
//If a field is empty, we need to disable "Next" button
cancellable = Publishers.CombineLatest4(bpPublishers[0], bpPublishers[1], bpPublishers[2], bpPublishers[3]).map { $0.isEmpty || $1.isEmpty || $2.isEmpty || $3.isEmpty }.assign(to: \.aFieldsisEmpty, on: self)
}
}
The idea is to create HStacks for each datasorce(sbp,dbp,pulse,weight) to look like this
struct HomeScreen: View {
#ObservedObject var viewModel = HomeViewModel()
var body: some View {
VStack {
ForEach(Range(0...3)) { index -> BPField in
BPField(input: self.$viewModel.dataSoure[index])
}
Button("Next", action: {
print("Take to the Detail screen")
}).disabled(self.viewModel.aFieldsisEmpty)
}.padding()
}
}
struct BPField: View {
#Binding var input: BPInput
var body: some View {
//implicit HStack
Text(input.field.name)
BPTextField(text: $input.value, placeHolder: input.field.name)//Error:- Cannot assign to property: 'value' is a get-only property
// input.value being read only I can't bind it. How to modify my model now so that I can bind it here?
}
}
And my custom TextField
struct BPTextField: View {
let keyboardType: UIKeyboardType = .numberPad
var style: some TextFieldStyle = RoundedBorderTextFieldStyle()
var text: Binding<String>
let placeHolder: String
// var onEdingChanged: (Bool) -> Void
// var onCommit: () -> ()
var background: some View = Color.white
var foregroundColor: Color = .black
var font: Font = .system(size: 14)
var body: some View {
TextField(placeHolder, text: text)
.background(background)
.foregroundColor(foregroundColor)
.textFieldStyle(style)
}
}
your problems are not there, what SwiftUI tells you.
but you should first compile "small parts" of your code and simplify it, so the compiler will tell you the real errors.
one is here:
BPTextField(text: self.$viewModel.dataSoure[index].value, placeHolder: viewModel.dataSoure[index].field.placeholder)
and the error is:
Cannot subscript a value of type 'Binding<[BPInput]>' with an argument of type 'WritableKeyPath<_, _>'
and of course you forgot the self ....