I have app with only two Views, both Views has own ViewModel. ViewA shows and manipulate objects from selectedNumbers array. ViewB owns all available objects(numbers) - in this view I want manipulate selectedNumbers array, that is used by ViewA.
I'm trying to find out, how to share these selectedNumbers array between these two ViewModels. I tried to use EnvironmentObject, StaticObject etc. But nothing works as I need. What approach should I use to achieve desired result. Thanks for help!
import SwiftUI
struct ViewA: View {
#ObservedObject var viewModel = ViewModelA()
var body: some View {
VStack {
Text("\(viewModel.number)")
.font(.largeTitle)
.padding()
.onTapGesture {
viewModel.showNext()
}
ViewB()
}
}
}
class ViewModelA: ObservableObject {
var numbers: [Int] = []
#Published var number: Int
var index = 0
init() {
number = numbers.isEmpty ? 0 : numbers[index]
}
func showNext() {
guard !numbers.isEmpty else { return }
if index < numbers.count - 1 {
index += 1
} else {
index = 0
}
number = numbers[index]
}
}
struct ViewB: View {
#ObservedObject var viewModel = ViewModelB()
var body: some View {
HStack {
ForEach(viewModel.numbers, id: \.self) { number in
Text("\(number)")
.foregroundColor(viewModel.selectedNumbers.contains(number) ? .red : .black)
.onTapGesture {
viewModel.updateSelection(number)
}
}
}
}
}
class ViewModelB: ObservableObject {
#Published var numbers: [Int] = []
#Published var selectedNumbers: [Int] = []
init() {
numbers.append(contentsOf: [1,2,3,4,5,6,7,8])
}
func updateSelection(_ number: Int) {
if selectedNumbers.contains(number) {
selectedNumbers.remove(number)
} else {
selectedNumbers.append(number)
}
}
}
extension Array where Element: Equatable {
mutating func remove(_ object: Element) {
guard let index = firstIndex(of: object) else {return}
remove(at: index)
}
}
You can still keep the logic separate, but you need to keep a single source of truth, and if you want to share data among views, you either need to pass Bindings or you can also share #ObservedObject among Subviews.
import SwiftUI
struct ViewA: View {
#ObservedObject var viewModel = ViewModelA(modelB: ViewModelB())
var body: some View {
VStack {
Text("\(viewModel.number)")
.font(.largeTitle)
.padding()
.onTapGesture {
viewModel.showNext()
}
ViewB(model: viewModel)
}
}
}
class ViewModelA: ObservableObject {
var numbers: [Int] = []
#Published var number: Int
#Published var modelB:ViewModelB
var index = 0
init(modelB:ViewModelB) {
self.modelB = modelB
number = numbers.isEmpty ? 0 : modelB.selectedNumbers[index]
}
func showNext() {
guard !modelB.selectedNumbers.isEmpty else { return }
if index < modelB.selectedNumbers.count - 1 {
index += 1
} else {
index = 0
}
number = modelB.selectedNumbers[index]
}
}
struct ViewB: View {
#ObservedObject var model : ViewModelA
var body: some View {
HStack {
ForEach(model.modelB.selectedNumbers, id: \.self) { number in
Text("\(number)")
.foregroundColor(model.modelB.selectedNumbers.contains(number) ? .red : .black)
.onTapGesture {
model.modelB.updateSelection(number)
}
}
}
}
}
struct ViewModelB {
var selectedNumbers: [Int] = []
init() {
selectedNumbers.append(contentsOf: [1,2,3,4,5,6,7,8])
}
mutating func updateSelection(_ number: Int) {
if selectedNumbers.contains(number) {
selectedNumbers.remove(number)
} else {
selectedNumbers.append(number)
}
}
}
extension Array where Element: Equatable {
mutating func remove(_ object: Element) {
guard let index = firstIndex(of: object) else {return}
remove(at: index)
}
}
Related
I have a class that has a calculated property, which is an array consisting of instances of the structure.
struct Team: Identifiable, Codable, Hashable {
var id = UUID()
var name : String
}
class TeamRow : ObservableObject {
#Published var teamsArray : [Team] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(teamsArray) {
UserDefaults.standard.setValue(encoded, forKey: "Teams")
}
}
}
init() {
if let teams = UserDefaults.standard.data(forKey: "Teams") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Team].self, from: teams) {
self.teamsArray = decoded
return
}
}
}
}
Also, I have a view, with the ability to add elements(teams) to this array using a sheet.
struct PlayersRow: View {
#ObservedObject var teams = TeamRow()
#State private var team = ""
#State private var showTeamAddSheet = false
var body: some View {
Form {
ForEach(teams.teamsArray) { team in
Text(team.name)
.font(.system(size: 20))
.padding(.horizontal, 110)
.padding(.vertical, 10)
}
}
.navigationBarTitle("Teams")
.navigationBarItems(trailing: Button(action: {
self.showTeamAddSheet = true
}) {
Image(systemName: "plus")
.foregroundColor(.black)
.font(.system(size: 30))
})
.sheet(isPresented: $showTeamAddSheet) {
AddPlayerView(teams: self.teams)
}
}
}
This is a sheet view.
struct AddPlayerView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var teams : TeamRow
#State private var team = ""
var body: some View {
NavigationView {
Form {
TextField("Add new team", text: $team)
}
.navigationBarItems(trailing: Button(action: {
let newTeam = Team(name: self.team)
self.teams.teamsArray.append(newTeam)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Сохранить")
.font(.custom("coolJazz", size: 20))
.foregroundColor(.black)
}))
.navigationBarTitle("Add Team")
}
}
}
And I have a view where I need to output the array elements one by one, using a button, clicked on the button, the view screen displayed 0 element, clicked on the button, displayed first element, etc.
struct GameView: View {
#ObservedObject var teams = TeamRow()
//#State var index = 0
var body: some View {
VStack(spacing: 40) {
//Text(teams.teamsArray[index]) this isn't worked, return an error: Initializer 'init(_:)' requires that 'Team' conform to 'StringProtocol'
Button(action: {
}) {
Text("press it")
}
}
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView().environmentObject(TeamRow())
}
}
if the array is set initially in the class, I have no problem displaying the elements on the screen by increasing the index, but how to solve this problem I do not know...
Can some one explain newbie?
Change your GameView Code to following:
struct GameView: View {
#ObservedObject var teams = TeamRow()
#State var index = 0
var body: some View {
VStack(spacing: 40) {
if teams.teamsArray.count > index {
Text(teams.teamsArray[index].name)
}
Button(action: {
index += 1
}) {
Text("press it")
}
}
}
}
Hi my problem is that i want to sort an array of objects by the object's title property. When i change the title's type from String to LocalizedStringKey i get an error. Is there a way to sort the correct translated string behind the localizedStringKey.
struct Item: Codable,Comparable, Hashable, Identifiable {
static func < (lhs: Item, rhs: Item) -> Bool {
return lhs.title < rhs.title
}
var id: Int
let image: String
let color: String
// title should be LocalizedStringKey
let title: String
}
......
#State private var sortedDown = false
var filteredItems: [Item] {
var sortedItems: [Item]
let filteredItems = modelData.items.filter { item in
(!showFavoritesOnly || item.isFavorite)
}
if sortedDown {
sortedItems = filteredItems.sorted(by: { (item1, item2) -> Bool in
return item1.title > item2.title
})
} else {
sortedItems = filteredItems.sorted(by: { (item1, item2) -> Bool in
return item1.title < item2.title
})
}
return sortedItems
}
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly, label: {
Text("showFavorites")
})
ForEach(filteredItems) { (item) in
.....
``
Don't forget to provide a minimal reproducible example when you ask questions. You will get better and more focused answers. Below is one way you can do it.
class LocalizeTitleViewModel: ObservableObject {
#Published var items: [SampleItem] = [SampleItem(title: "orange", isFavorite: true), SampleItem(title: "pink", isFavorite: false), SampleItem(title: "red", isFavorite: true)]
#Published var sortedDown = false
#Published var showFavoritesOnly = false
var filteredItems: [SampleItem] {
var sortedItems: [SampleItem]
let filteredItems = items.filter { item in
(!showFavoritesOnly || item.isFavorite)
}
if sortedDown {
sortedItems = filteredItems.sorted(by: { (item1, item2) -> Bool in
return item1.localizedTitle > item2.localizedTitle
})
} else {
sortedItems = filteredItems.sorted(by: { (item1, item2) -> Bool in
return item1.localizedTitle < item2.localizedTitle
})
}
return sortedItems
}
}
struct LocalizeTitle: View {
#StateObject var modelData: LocalizeTitleViewModel = LocalizeTitleViewModel()
var body: some View {
NavigationView {
List {
Toggle(isOn: $modelData.showFavoritesOnly, label: {
Text("showFavorites")
})
ForEach(modelData.filteredItems) { (item) in
Text(item.localizedTitle)
}
}
}
}
}
struct SampleItem: Identifiable {
var id: UUID = UUID()
let title: String
var isFavorite: Bool
var localizedTitle: String{
get{
//You need a Localizable.strings file in your project that contains all your `title`
return NSLocalizedString(self.title, comment: "Sample Item title")
}
}
}
struct LocalizeTitle_Previews: PreviewProvider {
static var previews: some View {
LocalizeTitle()
}
}
LocalizedStringKey does not conform to the Comparable protocol, so you cannot use the < or > operators on it.
You can either just use a regular String, or use custom comparison logic.
I saw many blogs/articles on how to build a timer with Swift UI. But I cannot figure out to get what I really want working.
The 2 main issues I am facing are:
1. My TimerView is rebuilt whenever its parent view is rebuilt due to states changing
2. I am not able to send parameters to my #ObservedObject TimeCountDown property (2 parameters: duration coming from another view, and an onEnded completion)
class Round: ObservableObject {
#Publishedvar items: [Item]
var duration: Double
init(items: [Item], duration: Double) {
self.items = items
self.duration = duration
}
}
Struct ParentView: View {
#ObservedObject var round: Round
#State private var isRoundTerminated: Bool = false
var body: Some View {
VStack {
if isRoundTerminated {
RoundEndView()
} else {
TimerView(duration: round.duration, onEnded: onTimerTerminated)
RoundPlayView(items: round.items)
}
}
}
}
struct TimerView: View {
#ObservedObject countdown = TimeCountDown()
var duration: Double
var onEnded: (() -> Void)?
///// I DO NOT KNOW HOW TO PROPAGATE THE onEnded completion TO countdown:TimeCountDown
var body: Some View {
Text("There are \(countdown.remainingTime) remaining secs")
.onAppend() {
timer.start(duration: duration)
/// MAYBE I COULD ADD THE onEnded WITHIN THE start() CALL?
}
}
}
class TimeCountDown : ObservableObject {
var timer : Timer!
#Published var remainingTime: Double = 60
var onEnded: (() -> Void)?
init(onEnded: #escaping (() -> Void)?) {
self.onEnded = onEnded
}
func start(duration: Double) {
self.timer?.invalidate()
self.remainingTime = duration
timer = Timer.scheduledTimer(timeInterval:1, target: self, selector:#selector(updateCountdown), userInfo: nil, repeats: true)
}
#objc private func updateCount() {
remainingTime -= 1
if remainingTime <= 0 {
killTimer()
self.onEnded?()
}
}
private func killTimer() {
timer?.invalidate()
timer = nil
}
}
However that does not work...
I also tried to implement the following TimerView:
struct CountdownView: View {
#State private var remainingTime: Int = 60
#Binding var countingDown: Bool
var onEnded: (() -> Void)?
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(durations: TimerDuration, onEnded: (() -> Void)?, start: Binding<Bool>) {
self.onEnded = onEnded
self._countingDown = start
self.remainingTime = durations.duration
}
var body: some View {
Text("Remaining \(remainingTime) secs")
.onReceive(timer) {_ in
if self.countingDown {
if self.remainingTime > 0 {
self.remainingTime -= 1
} else {
self.onTerminated()
}
}
}
}
func onTerminated() {
timer.upstream.connect().cancel()
self.remainingTime = 0
onEnded?()
}
}
However when the ParentView is rebuilt very often (due to modifications to round.items (#Published from Round:ObservableObject) the timer can be frozen.
#George_E, please find below my update code (Please note that I simplified the nextWord() func (a but useless here) but it does modify the round.remainingDeck which is a Published property within the ObervableObject Round. That triggers a rebuild of RoundPlayView and that freezes the timer):
import SwiftUI
import Combine
class Round: ObservableObject {
#Published var remainingDeck: [String] = ["Round#1", "Round#2", "Round#3", "Round4"]
var roundDuration: Int = 5
}
struct ContentView: View {
#ObservedObject var round = Round()
var body: Some View {
RoundPlayView(round: round)
}
}
struct RoundPlayView: View {
#ObservedObject var round: Round
var duration: Int {
return round.roundDuration
}
#State private var timerStart: Bool = true
#State private var isTerminated: Bool = false
init(round: Round) {
self.round = round
}
var body: some View {
return VStack {
if self.isTerminated {
EmptyView()
} else {
VStack {
CountdownView(duration: duration, onEnded: timerTerminated, start: $timerStart)
Button(action: { self.addWord() }) {
Text("Add words to the deck")
}
}
}
}
}
private func nextWord() {
round.remainingDeck.append("New Word")
}
private func timerTerminated() {
terminateRound()
}
private func terminateRound() {
self.isTerminated = true
}
}
and here is my TimerView code:
struct CountdownView: View {
#State private var remainingTime: Int = 60
#Binding var countingDown: Bool
var onEnded: (() -> Void)?
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(duration: Int, onEnded: (() -> Void)?, start: Binding<Bool>) {
self.onEnded = onEnded
self._countingDown = start
self.remainingTime = duration
}
var body: some View {
Text("Remaining \(remainingTime) secs")
.onReceive(timer) {_ in
if self.countingDown {
if self.remainingTime > 0 {
self.remainingTime -= 1
} else {
self.onTerminated()
}
}
}
}
func onTerminated() {
timer.upstream.connect().cancel()
self.remainingTime = 0
onEnded?()
}
}
I would like to know how to add an item from a List included in a Modal so when I tap in a row, I can have the item selected in the First View that launched the sheet (to make clearer, the effect the you find when the iPhone app "Messages" select a contact from "Contacts").
Here's my basic code
struct Product : Hashable {
var name : String
init(name: String) {
self.name = name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager {
var product : [Product] = []
init() {
let pencil = Product(name: "Pencil")
let eraser = Product(name: "Eraser")
let ruler = Product(name: "Notebook")
product = [pencil, eraser, ruler]
}
}
struct FirstView: View {
#State var isSheetOpened = false
var products : Manager
var body: some View {
VStack {
Button(action: {
self.isSheetOpened.toggle()
}) {
Text("Add item from sheet")
}
.sheet(isPresented: self.$isSheetOpened) {
Sheet(products: self.products, isSheetOpened: self.isSheetOpened)
}
Text("Add here")
}
}
}
struct Sheet: View {
var products : Manager
var isSheetOpened : Bool
var body: some View {
VStack {
List {
ForEach(self.products.product, id: \.self) { index in
Text(index.name)
}
}
}
}
}
You would need to use #State in FirstView and #Binding to Sheet to show the selected item in FirstView.
Additionally, to dismiss the view after selecting the item in the Sheet, you can use the environment variable presentationMode.
Here's the code that does it. Hope it helps.
struct FirstView: View {
#State var isSheetOpened = false
#State var selectedProduct: String = ""
var products = Manager()
var body: some View {
VStack {
Button(action: {
self.isSheetOpened.toggle()
}) {
Text("Add item from sheet")
}
.sheet(isPresented: self.$isSheetOpened) {
Sheet(products: self.products, isSheetOpened: self.isSheetOpened, selectedProduct: self.$selectedProduct)
}
Text("\(selectedProduct)")
}
}
}
struct Sheet: View {
var products : Manager
var isSheetOpened : Bool
#Binding var selectedProduct: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
List {
ForEach(self.products.product, id: \.self) { index in
Button(action: {
self.selectedProduct = index.name
self.presentationMode.wrappedValue.dismiss()
}) {
Text(index.name)
}
}
}
}
}
}
check this out:
Because you are obviously missing some basic knowledge you should read about #Binding, ObservableObject, EnvironmentObject ...without it you will never be able to write an app in SwiftUI
import SwiftUI
struct Product : Hashable {
var name : String
init(name: String) {
self.name = name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager : ObservableObject {
#Published var chosenProducts : [Product] = []
var products : [Product] = []
init() {
let pencil = Product(name: "Pencil")
let eraser = Product(name: "Eraser")
let ruler = Product(name: "Notebook")
products = [pencil, eraser, ruler]
}
}
struct ContentView: View {
#EnvironmentObject var manager : Manager
#State var isSheetOpened = false
var body: some View {
VStack {
Button(action: {
self.isSheetOpened.toggle()
}) {
Text("Add item from sheet")
}
Text("Chosen products")
.font(.largeTitle)
List {
ForEach(self.manager.chosenProducts, id: \.self) { product in
Text(product.name)
}
}
.sheet(isPresented: self.$isSheetOpened) {
Sheet(isSheetOpened: self.$isSheetOpened)
.environmentObject(self.manager)
}
Text("Add here")
}
}
}
struct Sheet: View {
#EnvironmentObject var manager : Manager
#Binding var isSheetOpened : Bool
var body: some View {
VStack {
List (self.manager.products, id: \.self) { product in
Button(action: {
self.manager.chosenProducts.append(product)
self.isSheetOpened = false
}) {
Text(product.name)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Manager())
}
}
I still have the problem when the count reaches 3, the reset function only stops it, but the count is not set to 0. I use the reset function with a button, it works perfectly. i would like to understand it and hope someone knows the reason for it?
import SwiftUI
import Combine
import Foundation
class WaitingTimerClass: ObservableObject {
#Published var waitingTimerCount: Int = 0
var waitingTimer = Timer()
func start() {
self.waitingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.waitingTimerCount += 1 }}
func stop() { waitingTimer.invalidate() }
func reset() { waitingTimerCount = 0; waitingTimer.invalidate() }
}
struct ContentView: View {
#ObservedObject var observed = WaitingTimerClass()
var body: some View {
VStack {
Text("\(self.observed.waitingTimerCount)")
.onAppear { self.observed.start() }
.onReceive(observed.$waitingTimerCount) { count in
guard count == 3 else {return}
self.observed.reset() // does not work
}
Button(action: {self.observed.start()}) {
Text("Start") }
Button(action: {self.observed.reset()}) { // works
Text("Reset") }
Button(action: {self.observed.stop()}) {
Text("Stop") }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It is because reset changes property affecting UI during body construction, so ignored. It should be changed as below
func reset() {
waitingTimer.invalidate()
DispatchQueue.main.async {
self.waitingTimerCount = 0
}
}