index out of bound from TabViewStyle of SwiftUI - ios

I am trying to implement a tabView that takes a list of items into pages that I could swipe back and forth. However, it keeps bugging out with an "index out of bound" error. It's confusing to me because I never set index at all, and I don't know how to force an index either...
Below are my code. Apologize for any naive code, I am new to SwiftUI. Any helps are appreciated, thank you!
import SwiftUI
//#Published private var list = QuestionList.self
struct QuestionList: Codable {
var list:[QuestionItem]
}
class QuestionItem: Codable, Identifiable {
var id: Int
var text: String
var type: Int
var answer: String
}
struct ContentView: View {
#State private var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
isShowForm = !isShowForm
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
print(type(of: qlist[1]))
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
self.showForm()
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach(qlist.indices, id: \.self) { index in
if qlist[index].type == 0 {
HStack {
Text("\(self.qlist[index].text)")
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text("\(self.qlist[index].text)")
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qlist[index].answer) {
}
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

It is not recommended to use indices in ForEach loops, instead use something like this updated code
that allows you to use your TextField :
ForEach($qlist) { $qItem in // <-- here $
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer) {
}
}
}
}
and as I mentioned in my comment, remove the print(qlist[1]) and print(type(of: qlist[1])) in showForm,
because if qlist is empty or only has one element, you will get the index out of bound error .
Remember one element is qlist[0].
EDIT-1: full test code:
this is the code I used in my test. It does not give any index errors.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
// print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
// print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
// print(type(of: qlist[1]))
isShowForm.toggle() // <--- here important
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
// self.showForm()
// simulate showForm()
qlist = [
QuestionItem(id: 0, text: "text0", type: 0, answer: "0"),
QuestionItem(id: 1, text: "text1", type: 1, answer: "1"),
QuestionItem(id: 2, text: "text2", type: 2, answer: "2"),
QuestionItem(id: 3, text: "text3", type: 3, answer: "3")
]
isShowForm.toggle() // <--- here after qlist is set
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach($qlist) { $qItem in
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer)
.border(.red)
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct QuestionItem: Codable, Identifiable { // <-- here note the struct
var id: Int
var text: String
var type: Int
var answer: String
}

Related

SwiftUI cannot update CoreData Entity Object

I am facing a very weird bug, I have a edit function which will accept the object and new form data from the frontend, the function can be executed without errors but the issue is the data is not updated at all. I tried reopen the simulator and refetch also no use to this. May I ask how to solve this?
class RoomDataController: ObservableObject {
#Published var rooms: [Room] = []
let container = NSPersistentContainer(name: "RentalAppContainer")
init() {
container.loadPersistentStores { description, error in
let current = FileManager.default.currentDirectoryPath
print(current)
if let error = error {
print("Failed to load data in DataController \(error.localizedDescription)")
}
}
fetchRooms()
}
func save(context: NSManagedObjectContext) {
do {
try context.save()
print("Data created or updated successfully!")
} catch {
// Handle errors in our database
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
func editRoom(room: Room, roomType: String, roomDescription: String, contactPeriod: String, roomPrice: Float, userID: UUID, isShort: Bool, isLong: Bool, images: String, email: String, phoneNum:String, context: NSManagedObjectContext) {
room.roomType = roomType
room.roomPrice = roomPrice
room.roomDescription = roomDescription
room.contactPeriod = contactPeriod
room.contactEmail = email
room.contactNum = phoneNum
room.updated_at = Date()
print("room to be edited: ", room)
save(context: container.viewContext)
fetchRooms()
}
}
The Frontend View that call the edit function
struct EditRoomView: View {
#EnvironmentObject var userAuth: UserAuth
#State var type: String = ""
#State var description: String = ""
#State var price: String = ""
#State var contactPeriod: String = ""
#State var isShort: Bool = false
#State var isLong: Bool = false
#State var email: String = ""
#State var phoneNum: String = ""
#ObservedObject var room: Room
private func editItem() {
RoomDataController().editRoom(
room: room,
roomType: self.type,
roomDescription: self.description,
contactPeriod: self.contactPeriod,
roomPrice: Float(self.price)!,
userID: userAuth.user.id!,
isShort: self.isShort,
isLong: self.isLong,
images: "room-1,room-2,room-3",
email: email,
phoneNum: phoneNum
)
}
init(room: Room){
self.room = room
_type = State(initialValue: room.roomType!)
_description = State(initialValue: room.roomDescription!)
_price = State(initialValue: String( room.roomPrice))
_contactPeriod = State(initialValue: room.contactPeriod!)
_email = State(initialValue: room.contactEmail!)
_phoneNum = State(initialValue: room.contactNum!)
if room.contactPeriod == "Short"{
_isShort = State(initialValue:true)
_isLong = State(initialValue: false)
} else {
_isShort = State(initialValue: false)
_isLong = State(initialValue:true)
}
}
var body: some View {
VStack{
HStack {
Spacer()
VStack {
Menu {
Button {
// self.selectedTab = .AccountViewTab
} label: {
NavigationLink(destination: AccountView().environmentObject(userAuth), label: {Text("My Profile")})
Image(systemName: "arrow.down.right.circle")
}
Button {
userAuth.isLoggedin = false
} label: {
Text("Log Out")
Image(systemName: "arrow.up.and.down.circle")
}
} label: {
Text("Menu").foregroundColor(Color.black)
}
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView().environmentObject(userAuth), label: {Text("Room Rent").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView(nextView: "PostRoomHistoryViewTab").environmentObject(userAuth), label: {Text("Post History").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView(nextView: "CustomerOrderViewTab").environmentObject(userAuth), label: {Text("Customer Orders").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
}
.padding(.bottom)
.background(Color.gray.edgesIgnoringSafeArea(.all))
Text("Edit Room Details")
Form {
Section(header: Text("Room Type") ) {
TextField("", text: self.$type)
}
Section(header: Text("Room Description") ) {
TextField("", text: self.$description)
}
MultipleImagePickerView()
Section(header: Text("Room Price") ) {
TextField("", text: self.$price)
}
Section(header: Text("Contact Period") ) {
HStack{
Toggle( "Short", isOn: $isShort )
Toggle("Long", isOn: $isLong )
}
}
Section(header: Text("Contact Email Address * :") ) {
TextField("", text: self.$email)
}
Section(header: Text("Contact Mobile No * :") ) {
TextField("", text: self.$phoneNum)
}
}
Button(action: {
self.editItem()
}) {
HStack {
Text("Update")
}
.padding()
.scaledToFit()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
}
}
}
Ok..Managed to solve this in this way, buy why?
do {
fetchRequest.predicate = NSPredicate(format: "id == %#", room.id?.uuidString as! CVarArg)
let result = try container.viewContext.fetch(fetchRequest)
result[0].roomType = roomType
result[0].roomPrice = roomPrice
result[0].roomDescription = roomDescription
result[0].contactPeriod = contactPeriod
result[0].contactEmail = email
result[0].contactNum = phoneNum
result[0].updated_at = Date()
} catch {
print ("fetch task failed", error)
}
if room.ownerID == userID.uuidString {
save(context: container.viewContext)
fetchRooms()
}

How would I get persistent data working in my reminder app

I have a reminder app that I am trying to implement persistent data in but whenever I close the app no data is saved. I know how to make it work with a normal MVC but I would like to get it working with the view model that I have.
I think I know what needs to change to fix the problem but I am not sure how to get to the solution. I am pretty sure that in the ReminderApp file under the NavigationView where it says HomeViewModel(reminds: store.reminds) I think that the store.reminds part needs to be binded to with a $ at the beginning but when I try doing that it doesn't work and instead says that HomeViewModel reminds property expects Reminder instead of Binding.
ReminderStore loads and saves the reminders to a file with the reminders and HomeViewModel contains the reminders array and appends a reminder to the array when a user adds a new reminder.
If anyone knows how to get this working that would be great since I have been stuck on this. My minimal reproducable example code is below.
RemindersApp
import SwiftUI
#main
struct RemindersApp: App {
#StateObject private var store = ReminderStore()
var body: some Scene {
WindowGroup {
NavigationView {
HomeView(homeVM: HomeViewModel(reminds: store.reminds)) {
ReminderStore.save(reminds: store.reminds) { result in
if case .failure(let error) = result {
fatalError(error.localizedDescription)
}
}
}
.navigationBarHidden(true)
}
.onAppear {
ReminderStore.load { result in
switch result {
case .failure(let error):
fatalError(error.localizedDescription)
case .success(let reminds):
store.reminds = reminds
}
}
}
}
}
}
HomeView
import SwiftUI
struct HomeView: View {
#StateObject var homeVM: HomeViewModel
#Environment(\.scenePhase) private var scenePhase
#State var addView = false
let saveAction: ()->Void
var body: some View {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
Text(remind.title)
}
}
}
.safeAreaInset(edge: .top) {
HStack {
Text("Reminders")
.font(.title)
.padding()
Spacer()
Button(action: {
addView.toggle()
}) {
Image(systemName: "plus")
.padding()
.font(.title2)
}
.sheet(isPresented: $addView) {
NavigationView {
VStack {
Form {
TextField("Title", text: $homeVM.newRemindData.title)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
homeVM.newRemindData = Reminder.Data()
addView.toggle()
}
}
ToolbarItem(placement: .principal) {
Text("New Reminder")
.font(.title3)
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
homeVM.addRemindData(remindData: homeVM.newRemindData)
addView.toggle()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView(homeVM: HomeViewModel(reminds: Reminder.sampleReminders), saveAction: {})
}
}
ReminderStore
import Foundation
import SwiftUI
class ReminderStore: ObservableObject {
#Published var reminds: [Reminder] = []
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("reminds.data")
}
static func load(completion: #escaping (Result<[Reminder], Error>) -> Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
completion(.success([]))
}
return
}
let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
DispatchQueue.main.async {
completion(.success(reminds))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
static func save(reminds: [Reminder], completion: #escaping (Result<Int, Error>) -> Void) {
do {
let data = try JSONEncoder().encode(reminds)
let outfile = try fileURL()
try data.write(to: outfile)
DispatchQueue.main.async {
completion(.success(reminds.count))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
HomeViewModel
import Foundation
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder]
#Published var newRemindData = Reminder.Data()
init(reminds: [Reminder]) {
self.reminds = reminds
}
func addRemindData(remindData: Reminder.Data) {
let newRemind = Reminder(data: remindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
}
Reminder
import Foundation
struct Reminder: Identifiable, Codable {
var title: String
let id: UUID
init(title: String, id: UUID = UUID()) {
self.title = title
self.id = id
}
}
extension Reminder {
struct Data {
var title: String = ""
var id: UUID = UUID()
}
var data: Data {
Data(title: title)
}
mutating func update(from data: Data) {
title = data.title
}
init(data: Data) {
title = data.title
id = UUID()
}
}
extension Reminder {
static var sampleReminders = [
Reminder(title: "Reminder1"),
Reminder(title: "Reminder2"),
Reminder(title: "Reminder3")
]
}
The reason you are struggeling here is because you try to have multiple Source of truth.
documentation on dataflow in SwiftUI
You should move the code from HomeViewModel to your ReminderStore and change the static functions to instance functions. This would keep your logic in one place.
You can pass your ReminderStore to your HomeView as an #EnvironmentObject
This would simplify your code to:
class ReminderStore: ObservableObject {
#Published var reminds: [Reminder] = []
#Published var newRemindData = Reminder.Data()
private func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("reminds.data")
}
func load() {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try self.fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
return
}
let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
DispatchQueue.main.async {
self.reminds = reminds
}
} catch {
DispatchQueue.main.async {
fatalError(error.localizedDescription)
}
}
}
}
func save() {
do {
let data = try JSONEncoder().encode(reminds)
let outfile = try fileURL()
try data.write(to: outfile)
} catch {
fatalError(error.localizedDescription)
}
}
func addRemindData() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
}
struct RemindersApp: App {
#StateObject private var store = ReminderStore()
var body: some Scene {
WindowGroup {
NavigationView {
HomeView() {
store.save()
}
.navigationBarHidden(true)
.environmentObject(store)
}
.onAppear {
store.load()
}
}
}
}
struct HomeView: View {
#Environment(\.scenePhase) private var scenePhase
#EnvironmentObject private var store: ReminderStore
#State var addView = false
let saveAction: ()->Void
var body: some View {
VStack {
List {
ForEach(store.reminds) { remind in
Text(remind.title)
}
}
}
.safeAreaInset(edge: .top) {
HStack {
Text("Reminders")
.font(.title)
.padding()
Spacer()
Button(action: {
addView.toggle()
}) {
Image(systemName: "plus")
.padding()
.font(.title2)
}
.sheet(isPresented: $addView) {
NavigationView {
VStack {
Form {
TextField("Title", text: $store.newRemindData.title)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
store.newRemindData = Reminder.Data()
addView.toggle()
}
}
ToolbarItem(placement: .principal) {
Text("New Reminder")
.font(.title3)
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
store.addRemindData()
addView.toggle()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
}
}
An issue I would recommend solving:
Naming a type after something that´s allready taken by Swift is a bad idea. You should rename your Data struct to something different.
ReminderStore.save isn't invoking in time.
By the time it invokes it doesn't have/get the reminder data.
That's the first thing I would make sure gets done. You may end up running into other issues afterward, but I would personally focus on that first.

How to setup NavigationLink inside SwiftUI list

I am attempting to set up a SwiftUI weather app. when the user searches for a city name in the textfield then taps the search button, a NavigationLink list item should appear in the list. Then, the user should be able to click the navigation link and re-direct to a detail view. My goal is to have the searched navigation links to populate a list. However, my search cities are not populating in the list, and I'm not sure why. In ContentView, I setup a list with a ForEach function that passes in cityNameList, which is an instance of the WeatherViewModel. My expectation is that Text(city.title) should display as a NavigationLink list item. How should I configure the ContentView or ViewModel to populate the the list with NavigationLink list items? See My code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityWeather, id: \.id) { city in
NavigationLink(destination: DetailView(detail: viewModel)) {
HStack {
Text(city.cityWeather.name)
.font(.system(size: 32))
}
}
}
}
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error)
}
}
task.resume()
}
}
Model
import Foundation
struct WeatherModel: Identifiable, Codable {
var id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DetailView
import SwiftUI
struct DetailView: View {
var detail: WeatherViewModel
var body: some View {
VStack(spacing: 20) {
Text(detail.cityWeather.name)
.font(.system(size: 32))
Text("\(detail.cityWeather.main.temp)")
.font(.system(size: 44))
Text(detail.cityWeather.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherViewModel.init())
}
}
try something like this example code, works well for me:
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
}
}
}
}
Spacer()
}.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name).font(.system(size: 32))
Text("\(detail.main.temp)").font(.system(size: 44))
Text(detail.firstWeatherInfo()).font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}

SwiftUI: How to select multi items(image) with ForEach?

I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.

SwiftUI/Combine no updates to #ObservedObject from #Published

I have a logon screen followed by an overview screen. When the user is successful at logon, the logon response sends back a list of items, which I want to display on the subsequent overview screen.
I can see the response being successfully mapped but the overview view is not receiving any update to the #ObservedObject. I could be missing something obvious but I've been through a bunch of articles and haven't managed to get anything working. Any help appreciated!
Logon view
import SwiftUI
struct LogonView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var inputError: Bool = false
#State private var errorMessage: String = ""
#State private var loading: Bool? = false
#State private var helpShown: Bool = false
#State private var successful: Bool = false
//MARK:- UIView
var body: some View {
NavigationView {
VStack {
VStack {
TextField("Username", text: $username)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 80)
VStack {
SecureField("Password", text: $password)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 40)
if (inputError) {
HStack {
Text(errorMessage)
.padding(.top)
.padding(.horizontal)
.foregroundColor(Color("SharesaveError"))
.animation(.easeInOut)
.lineLimit(nil)
.font(.footnote)
Spacer()
}
}
SharesaveButton(action: {self.submit(user: self.username, pass: self.password)},
label: "Log on",
loading: $loading,
style: .primary)
.padding(.top, 40)
.animation(.interactiveSpring())
NavigationLink(destination: OverviewView(), isActive: $successful) {
Text("")
}
Spacer()
}
.navigationBarTitle("Hello.")
.navigationBarItems(
trailing: Button(action: { self.helpShown = true }) {
Text("Need help?").foregroundColor(.gray)
})
.sheet(isPresented: $helpShown) {
SafariView( url: URL(string: "http://google.com")! )
}
}
}
//MARK:- Functions
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel()
resultsVM.getGrants(user: user, pass: pass,
successful: { response in
self.loading = false
if ((response) != nil) { self.successful = true }
},
error: { error in
self.inputError = true
self.loading = false
self.successful = false
switch error {
case 401:
self.errorMessage = "Your credentials were incorrect"
default:
self.errorMessage = "Something went wrong, please try again"
}
},
failure: { fail in
self.inputError = true
self.loading = false
self.successful = false
self.errorMessage = "Check your internet connection"
})
}
}
Results View Model
import Foundation
import Moya
import Combine
import SwiftUI
class ResultsViewModel: ObservableObject {
#Published var results: Results = Results()
func getGrants(
user: String,
pass: String,
successful successCallback: #escaping (Results?) -> Void,
error errorCallback: #escaping (Int) -> Void,
failure failureCallback: #escaping (MoyaError?) -> Void
)
{
let provider = MoyaProvider<sharesaveAPI>()
provider.request(.getSharesave(username: user, password: pass)) { response in
switch response.result {
case .success(let value):
do {
let data = try JSONDecoder().decode(Results.self, from: value.data)
self.results = data
successCallback(data)
} catch {
let errorCode = value.statusCode
errorCallback(errorCode)
}
case .failure(let error):
failureCallback(error)
}
}
}
}
Overview View
import SwiftUI
import Combine
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel()
var body: some View {
let text = "\(self.viewModel.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}
You submitted request to one instance of ResultsViewModel
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel() // << here
by try to read data from another instance of ResultsViewModel
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel() // << here
but it must be the one instance, so modify as follows
1) In OverviewView
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel // expects injection
2) In LogonView
struct LogonView: View {
#ObservedObject var resultsViewModel = ResultsViewModel() // created once
and inject same instance for OverviewView
NavigationLink(destination: OverviewView(viewModel: self.resultsViewModel), isActive: $successful) {
Text("")
}
and in submit
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = self.resultsViewModel // use created
Please try after change in initialise OverviewView like below in NavigationLink
NavigationLink(destination: OverviewView(viewModel: self.resultsVM),
isActive: $successful) {
Text("")
}
OR
pass results in OverviewView as argument like below
NavigationLink(destination: OverviewView(results: self.resultsVM.results),
isActive: $successful) {
Text("")
}
.....
struct OverviewView: View {
let results: Results
var body: some View {
let text = "\(self.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}

Resources