Updating swiftui text view by a function in another file - ios

I have a function that goes out to an API and gets a stock price. When I try to run the function updateView(), I just get 50 instead of 60 which is expected in the text view. The function updateView() can't change the text. Can someone tell me what I'm doing wrong? My code is below.
struct ContentView: View {
#State var stockPrice: Double = 0.0
var body: some View {
List {
HStack {
Text("Stock Price (15 Min. delay)")
Spacer()
Text("\(stockPrice)")
.onAppear() {
self.updateStockPrice(stock: 50)
}
}
}
}
public func updateStockPrice(stock: Int) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
DispatchQueue.main.async {
self.stockPrice = Double(stock)
}
}
}
}
// Assume function updateView is in another file.
func updateView() {
ContentView().stockPrice = 60.0
}

You have the following three problems with your code:
As pawello2222 mentioned, you need to set stockPrice on the same ContentView object that is being displayed. You will find it either in your AppDelegate or SceneDelegate.
#State does not publish changes. You need a #Published variable in an #ObservableObject.
onAppear() gets called when the view is updated on binding variable change. And so, your value will get overwritten. You need to find another place to call updateStockPrice() - the view initializer perhaps.
The following code snippet works in Playground. But depending on your real-world context, you will have to fix the above problems appropriately.
import SwiftUI
import PlaygroundSupport
class Model: ObservableObject {
#Published var stockPrice: Double = 0.0
}
struct ContentView: View {
#ObservedObject var model = Model()
init() {
self.updateStockPrice(stock: 50)
}
var body: some View {
List {
HStack {
Text("Stock Price (15 Min. delay)")
Spacer()
Text("\(model.stockPrice)")
}
}
}
public func updateStockPrice(stock: Int) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
DispatchQueue.main.async {
self.model.stockPrice = Double(stock)
}
}
}
}
// Assume function updateView is in another file.
func updateView(_ view: ContentView) {
view.model.stockPrice = 60.0
}
let view = ContentView()
updateView(view)
PlaygroundPage.current.setLiveView(view)

Related

NavigationLink skips to one specific navigation, no menu screen - also, Argument passed to call that takes no arguments

I am trying to create a navigation screen with two buttons for two view controllers.
I found a game online which has similar navigation implemented. But when I run it, it takes me straight to the first navigation item without showing the options screen.
I want to get the navigation working properly, but it does not.
Note: I'm getting an error on the second part (GameViewController.swift) on line 9 saying Argument passed to call that takes no arguments. It seems I need to move viewModel or something similar into ContentView but the different ways I've tried just pop up more errors that I don't understand.
import SwiftUI
import Foundation
// I think I need to reference viewModel below somehow, but I'm just not sure how
// I tried different ways but just always get more errors I don't understand yet
struct ContentView: View {
#State private var score = 0
var body: some View {
NavigationView {
Image("Clouds")
.overlay(
VStack(spacing: 30) {
NavigationLink(destination: ResultView2(choice: "MenuGrey")) {
Image("MenuGrey")
}
NavigationLink(destination: MyContentView(viewModel: GameViewModel(Engine.self as! Engine, storage: Storage.self as! Storage, stateTracker: StateTracker.self as! StateTracker))) {
Image("Menu")
}
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
struct MyContentView: View {
#ObservedObject var viewModel: GameViewModel
#State var showMenu = false
var body: some View {
VStack(alignment: .center, spacing: 16) {
Header(score: viewModel.state.score, bestScore: viewModel.bestScore, menuAction: {
self.showMenu.toggle()
}, undoAction: {
self.viewModel.undo()
}, undoEnabled: self.viewModel.isUndoable)
GoalText()
Board(board: viewModel.state.board, addedTile: viewModel.addedTile)
Moves(viewModel.numberOfMoves)
}
.frame(minWidth: .zero,
maxWidth: .infinity,
minHeight: .zero,
maxHeight: .infinity,
alignment: .center)
.background(Color.gameBackground)
.background(Menu())
.background(GameOver())
.edgesIgnoringSafeArea(.all)
}
}
extension MyContentView {
private func Menu() -> some View {
EmptyView().sheet(isPresented: $showMenu) {
MenuView(newGameAction: {
self.viewModel.reset()
self.showMenu.toggle()
}, resetScoreAction: {
self.viewModel.eraseBestScore()
self.showMenu.toggle()
})
}
}
private func GameOver() -> some View {
EmptyView().sheet(isPresented: $viewModel.isGameOver) {
GameOverView(score: self.viewModel.state.score, moves: self.viewModel.numberOfMoves) {
self.viewModel.reset()
}
}
}
}
struct ResultView2: View {
var choice: String
var body: some View {
Text("You chose MenuGrey")
}
}
The below is the GameViewController.swift part
import SwiftUI
import Foundation
class GameViewController: UIHostingController<ContentView> {
private let viewModel: GameViewModel?
init(viewModel: GameViewModel) {
self.viewModel = viewModel
super.init(rootView: ContentView(viewModel: viewModel))
// The line above gives error "Argument passed to call that takes no arguments"
setupGestures()
viewModel.start()
}
private func setupGestures() {
view.addGestureRecognizer(Swipe(.left) { [weak self] in
self?.viewModel?.push(.left)
})
view.addGestureRecognizer(Swipe(.right) { [weak self] in
self?.viewModel?.push(.right)
})
view.addGestureRecognizer(Swipe(.up) { [weak self] in
self?.viewModel?.push(.up)
})
view.addGestureRecognizer(Swipe(.down) { [weak self] in
self?.viewModel?.push(.down)
})
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Basically, that error is saying that you are trying to pass something to the ContentView (in this case you are passing an instance of GameViewModel while the ContentView does not expect that thing (in this case that instance), So to fix that we have to make the ContentView to take an instance of GameViewModel.
According to the code and explanation you provided, you need to do few things to fix correctly the error:
1. Create an instance variable of GameViewModel in your ContentView:
Add this in your ContentView
static let engine = GameEngine()
static let storage = LocalStorage()
static let stateTracker = GameStateTracker(initialState: (storage.board ?? engine.blankBoard, storage.score))
#ObservedObject var viewModel = GameViewModel(engine, storage: storage, stateTracker: stateTracker)
2. Change the NavigationLink in your ContentView:
Change
NavigationLink(destination: MyContentView(viewModel: GameViewModel(Engine.self as! Engine, storage: Storage.self as! Storage, stateTracker: StateTracker.self as! StateTracker))) {
Image("Menu")
}
to
NavigationLink(destination: MyContentView(viewModel: viewModel)) {
Image("Menu")
}
At this stage, the error should be gone.

.onReceive firing twice | SwiftUI

I have a SwiftUI view that includes a Picker. I'm using a Switch statement inside .onReceive of the Picker to call a function. The function calls an external API.
The problem is that the function is being called twice whenever the view is initialised i.e duplicating the data. I'm can't figure out why .onReceive is being called twice.
I think it might have something to do with the func being called when I init the Picker Model and then getting another notification from the Picker itself but I'm not sure how to work around it.
Here's my code:
Picker Model
import Foundation
class PickerModel: ObservableObject {
#Published var filter = 0
let pickerOptions = ["Popular", "Top Rated"]
}
View containing the Picker:
import SwiftUI
struct FilteredMoviesGridView: View {
#ObservedObject private var filteredMovieVM = FilteredMovieGridViewModel()
#ObservedObject private var pickerModel = PickerModel()
private var twoColumnGrid = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
NavigationView {
VStack {
Picker(selection: $pickerModel.filter, label: Text("Select")) {
ForEach(0 ..< pickerModel.pickerOptions.count) {
Text(pickerModel.pickerOptions[$0])
}
}.onReceive(pickerModel.$filter) { (value) in
switch value {
case 0:
filteredMovieVM.movies.removeAll()
filteredMovieVM.currentPage = 1
filteredMovieVM.fetchMovies(filter: "popularity")
case 1:
filteredMovieVM.movies.removeAll()
filteredMovieVM.currentPage = 1
filteredMovieVM.fetchMovies(filter: "vote_average")
default:
filteredMovieVM.movies.removeAll()
filteredMovieVM.currentPage = 1
filteredMovieVM.fetchMovies(filter: "popularity")
}
}.pickerStyle(SegmentedPickerStyle())
ScrollView {
LazyVGrid(columns: twoColumnGrid, spacing: 10) {
ForEach(filteredMovieVM.movies, id:\.id) { movie in
NavigationLink(destination: MovieDetailView(movie: movie)) {
MovieGridItemView(movies: movie)
}.buttonStyle(PlainButtonStyle())
.onAppear(perform: {
if movie == self.filteredMovieVM.movies.last {
switch pickerModel.filter {
case 0:
self.filteredMovieVM.checkTotalMovies(filter: "popularity")
case 1:
self.filteredMovieVM.checkTotalMovies(filter: "vote_average")
default:
self.filteredMovieVM.checkTotalMovies(filter: "popularity")
}
}
})
}
}
}
.navigationBarTitle("Movies")
}
}.accentColor(.white)
}
}
The View Model containing the function:
import Foundation
class FilteredMovieGridViewModel: ObservableObject {
#Published var movies = [Movie]()
private var filteredMovies = [MovieList]()
var currentPage = 1
func checkTotalMovies(filter: String) {
if filteredMovies.count < 20 {
fetchMovies(filter: filter)
}
}
func fetchMovies(filter: String) {
WebService().getMoviesByFilter(filter: filter, page: currentPage) { movie in
if let movie = movie {
self.filteredMovies.append(movie)
for movie in movie.movies {
self.movies.append(movie)
print(self.movies.count)
}
}
}
if let totalPages = filteredMovies.first?.totalPages {
if currentPage <= totalPages {
currentPage += 1
}
}
}
}
Any help would be greatly appreciated.
Most likely you're recreating your ObservedObjects whenever your FilteredMoviesGridView is recreated. This can happen whenever SwiftUI's runtime thinks it needs to recreate it. So your view creation should be cheap and you should make sure not to accidentally recreate resources you need. Luckily SwiftUI in iOS 14, etc. has made it much easier to fix this problem. Instead of using #ObservedObject, use #StateObject, which will keep the same instance alive as your view is recreated.

SwiftUI View not updating based on #ObservedObject

In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual objectWillChange.send() call. I would have thought that should trigger the update, but the screen never changes.
Any idea? I'd be interested in a solution using NumberLine as a class, or as a struct, or using no NumberLine type at all and instead rather just using an array variable within the ContentView struct.
Here's the code:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(0 ..< numberLine.visible.count) { number in
if self.numberLine.visible[number] {
Text(String(number)).font(.title).padding(5)
}
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.objectWillChange.send()
self.numberLine.visible[index].toggle()
print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")")
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
var visible: [Bool] = Array(repeatElement(true, count: 10))
}
With #ObservedObject everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send() and use instead default #Published pattern in view model
class NumberLinex: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach still not updated... yes, because problem in ForEach - you used constructor with Range that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
I faced the same issue.
For me, replacing #ObservedObject with #StateObject worked.
Using your insight, #Asperi, that the problem is with the ForEach and not with the #ObservableObject functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
There is nothing Wrong with observed object, you should use #Published in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
#ObservedObject var model = NumberLineModel()
#State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.

#Published and .assign not reacting to value update

SwiftUI and Combine noob here, I isolated in a playground the problem I am having. Here is the playground.
final class ReactiveContainer<T: Equatable> {
#Published var containedValue: T?
}
class AppContainer {
static let shared = AppContainer()
let text = ReactiveContainer<String>()
}
struct TestSwiftUIView: View {
#State private var viewModel = "test"
var body: some View {
Text("\(viewModel)")
}
init(textContainer: ReactiveContainer<String>) {
textContainer.$containedValue.compactMap {
print("compact map \($0)")
return $0
}.assign(to: \.viewModel, on: self)
}
}
AppContainer.shared.text.containedValue = "init"
var testView = TestSwiftUIView(textContainer: AppContainer.shared.text)
print(testView)
print("Executing network request")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
AppContainer.shared.text.containedValue = "Hello world"
print(testView)
}
When I run the playground this is what's happening:
compact map Optional("init")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil))
Executing network request
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil))
So as you can see, two problems there:
The compact map closure is only called once, on subscription but not when the dispatch is ran
The assign operator is never called
I have been trying to solve this these past few hours without any success. Maybe someone with a top knowledge in SwiftUI/Combine could help me, thx !
EDIT
Here is the working solution:
struct ContentView: View {
#State private var viewModel = "test"
let textContainer: ReactiveContainer<String>
var body: some View {
Text(viewModel).onReceive(textContainer.$containedValue) { (newContainedValue) in
self.viewModel = newContainedValue ?? ""
}
}
init(textContainer: ReactiveContainer<String>) {
self.textContainer = textContainer
}
}
I would prefer to use ObservableObject/ObservedObject pattern, right below, but other variants also possible (as provided further)
All tested with Xcode 11.2 / iOS 13.2
final class ReactiveContainer<T: Equatable>: ObservableObject {
#Published var containedValue: T?
}
struct TestSwiftUIView: View {
#ObservedObject var vm: ReactiveContainer<String>
var body: some View {
Text("\(vm.containedValue ?? "<none>")")
}
init(textContainer: ReactiveContainer<String>) {
self._vm = ObservedObject(initialValue: textContainer)
}
}
Alternates:
The following fixes your case (if you don't store subscriber the publisher is canceled immediately)
private var subscriber: AnyCancellable?
init(textContainer: ReactiveContainer<String>) {
subscriber = textContainer.$containedValue.compactMap {
print("compact map \($0)")
return $0
}.assign(to: \.viewModel, on: self)
}
Please note, view's state is linked only being in view hierarchy, in Playground like you did it holds only initial value.
Another possible approach, that fits better for SwiftUI hierarchy is
struct TestSwiftUIView: View {
#State private var viewModel: String = "test"
var body: some View {
Text("\(viewModel)")
.onReceive(publisher) { value in
self.viewModel = value
}
}
let publisher: AnyPublisher<String, Never>
init(textContainer: ReactiveContainer<String>) {
publisher = textContainer.$containedValue.compactMap {
print("compact map \($0)")
return $0
}.eraseToAnyPublisher()
}
}
I would save a reference to AppContainer.
struct TestSwiftUIView: View {
#State private var viewModel = "test"
///I just added this
var textContainer: AnyCancellable?
var body: some View {
Text("\(viewModel)")
}
init(textContainer: ReactiveContainer<String>) {
self.textContainer = textContainer.$containedValue.compactMap {
print("compact map \(String(describing: $0))")
return $0
}.assign(to: \.viewModel, on: self)
}
}
compact map Optional("init")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil), textContainer: Optional(Combine.AnyCancellable))
Executing network request
compact map Optional("Hello")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil), textContainer: Optional(Combine.AnyCancellable))
We don't use Combine for moving data between Views, SwiftUI already has built-in support for this. The main problem is you are treating the TestSwiftUIView as if it is a class but it is a struct, i.e. a value. It's best to think of the View simply as the data to be displayed. SwiftUI creates these data structs over and over again when data changes. So the solution is simply:
struct ContentView: View {
let text: String
var body: some View { // only called if text is different from last time ContentView was created in a parent View's body.
Text(text)
}
}
The parent body method can call ContentView(text:"Test") over and over again but the ContentView body method is only called by SwiftUI when the let text is different from last time, e.g. ContentView(text:"Test2"). I think this is what you tried to recreate with Combine but it is unnecessary because SwiftUI already does it.

SwiftUI: How to get continuous updates from Slider

I'm experimenting with SwiftUI and the Slider control like this:
struct MyView: View {
#State private var value = 0.5
var body: some View {
Slider(value: $value) { pressed in
}
}
}
I'm trying to get continuous updates from the Slider as the user drags it, however it appears that it only updates the value at the end of the value change.
Anyone played with this? know how to get a SwiftUI Slider to issue a stream of value changes? Combine perhaps?
In SwiftUI, you can bind UI elements such as slider to properties in your data model and implement your business logic there.
For example, to get continuous slider updates:
import SwiftUI
import Combine
final class SliderData: BindableObject {
let didChange = PassthroughSubject<SliderData,Never>()
var sliderValue: Float = 0 {
willSet {
print(newValue)
didChange.send(self)
}
}
}
struct ContentView : View {
#EnvironmentObject var sliderData: SliderData
var body: some View {
Slider(value: $sliderData.sliderValue)
}
}
Note that to have your scene use the data model object, you need to update your window.rootViewController to something like below inside SceneDelegate class, otherwise the app crashes.
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(SliderData()))
After much playing around I ended up with the following code. It's a little cut down to keep the answer short, but here goes. There was a couple of things I needed:
To read value changes from the slider and round them to the nearest integer before setting an external binding.
To set a localized hint value based on the integer.
struct AspectSlider: View {
// The first part of the hint text localization key.
private let hintKey: String
// An external integer binding to be set when the rounded value of the slider
changes to a different integer.
private let value: Binding<Int>
// A local binding that is used to track the value of the slider.
#State var sliderValue: Double = 0.0
init(value: Binding<Int>, hintKey: String) {
self.value = value
self.hintKey = hintKey
}
var body: some View {
VStack(alignment: .trailing) {
// The localized text hint built from the hint key and the rounded slider value.
Text(LocalizedStringKey("\(hintKey).\(self.value.value)"))
HStack {
Text(LocalizedStringKey(self.hintKey))
Slider(value: Binding<Double>(
getValue: { self.$sliderValue.value },
setValue: { self.sliderChanged(toValue: $0) }
),
through: 4.0) { if !$0 { self.slideEnded() } }
}
}
}
private func slideEnded() {
print("Moving slider to nearest whole value")
self.sliderValue = self.sliderValue.rounded()
}
private func sliderChanged(toValue value: Double) {
$sliderValue.value = value
let roundedValue = Int(value.rounded())
if roundedValue == self.value.value {
return
}
print("Updating value")
self.value.value = roundedValue
}
}
We can go without custom bindings, custom inits, ObservableObjects, PassthroughSubjects, #Published and other complications. Slider has .onChange(of: perform:) modifier which is perfect for this case.
This answer can be rewritten as follows:
struct AspectSlider2: View {
#Binding var value: Int
let hintKey: String
#State private var sliderValue: Double = 0.0
var body: some View {
VStack(alignment: .trailing) {
Text(LocalizedStringKey("\(hintKey)\(value)"))
HStack {
Slider(value: $sliderValue, in: 0...5)
.onChange(of: sliderValue, perform: sliderChanged)
}
}
}
private func sliderChanged(to newValue: Double) {
sliderValue = newValue.rounded()
let roundedValue = Int(sliderValue)
if roundedValue == value {
return
}
print("Updating value")
value = roundedValue
}
}
In Version 11.4.1 (11E503a) & Swift 5. I didn't reproduce it.
By using Combine, I could get continuously update from slider changes.
class SliderData: ObservableObject {
#Published var sliderValue: Double = 0
...
}
struct ContentView: View {
#ObservedObject var slider = SliderData()
var body: some View {
VStack {
Slider(value: $slider.sliderValue)
Text(String(slider.sliderValue))
}
}
}
I am not able to reproduce this issue on iOS 13 Beta 2. Which operating system are you targeting?
Using a custom binding, the value is printed for every small change, not only after editing ended.
Slider(value: Binding<Double>(getValue: {0}, setValue: {print($0)}))
Note, that the closure ({ pressed in }) only reports when editing end starts and ends, the value stream is only passed into the binding.
What about like this:
(1) First you need the observable ...
import SwiftUI
import PlaygroundSupport
// make your observable double for the slider value:
class SliderValue: ObservableObject {
#Published var position: Double = 11.0
}
(2) When you make the slider, you have to PASS IN an instance of the observable:
So in HandySlider it is declared as an ObservedObject. (Don't forget, you're not "making" it there. Only declare it as a StateObject where you are "making" it.)
(3) AND you use the "$" for the Slider value as usual in a slider
(It seems the syntax is to use it on the "whole thing" like this "$sliderValue.position" rather than on the value per se, like "sliderValue.$position".)
struct HandySlider: View {
// don't forget to PASS IN a state object when you make a HandySlider
#ObservedObject var sliderValue: SliderValue
var body: some View {
HStack {
Text("0")
Slider(value: $sliderValue.position, in: 0...20)
Text("20")
}
}
}
(4) Actually make the state object somewhere.
(So, you use "StateObject" to do that, not "ObservedObject".)
And then
(5) use it freely where you want to display the value.
struct ContentView: View {
// here we literally make the state object
// (you'd just make it a "global" but not possible in playground)
#StateObject var sliderValue = SliderValue()
var body: some View {
HandySlider(sliderValue: sliderValue)
.frame(width: 400)
Text(String(sliderValue.position))
}
}
PlaygroundPage.current.setLiveView(ContentView())
Test it ...
Here's the whole thing to paste in a playground ...
import SwiftUI
import PlaygroundSupport
class SliderValue: ObservableObject {
#Published var position: Double = 11.0
}
struct HandySlider: View {
#ObservedObject var sliderValue: SliderValue
var body: some View {
HStack {
Text("0")
Slider(value: $sliderValue.position, in: 0...20)
Text("20")
}
}
}
struct ContentView: View {
#StateObject var sliderValue = SliderValue()
var body: some View {
HandySlider(sliderValue: sliderValue)
.frame(width: 400)
Text(String(sliderValue.position))
}
}
PlaygroundPage.current.setLiveView(ContentView())
Summary ...
You'll need an ObservableObject class: those contain Published variables.
Somewhere (obviously one place only) you will literally make that observable object class, and that's StateObject
Finally you can use that observable object class anywhere you want (as many places as needed), and that's ObservedObject
And in a slider ...
In the tricky case of a slider in particular, the desired syntax seems to be
Slider(value: $ooc.pitooc, in: 0...20)
ooc - your observable object class
pitooc - a property in that observable object class
You would not create the observable object class inside the slider, you create it elsewhere and pass it in to the slider. (So indeed in the slider class it is an observed object, not a state object.)
iOS 13.4, Swift 5.x
An answer based on Mohammid excellent solution, only I didn't want to use environmental variables.
class SliderData: ObservableObject {
let didChange = PassthroughSubject<SliderData,Never>()
#Published var sliderValue: Double = 0 {
didSet {
print("sliderValue \(sliderValue)")
didChange.send(self)
}
}
}
#ObservedObject var sliderData:SliderData
Slider(value: $sliderData.sliderValue, in: 0...Double(self.textColors.count))
With a small change to ContentView_Preview and the same in SceneDelegate.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(sliderData: SliderData.init())
}
}
If the value is in a navigation, child view:
Here's the case if the slider is, say, a popup which allows you to adjust a value.
It's actually simpler, nothing needs to be passed in to the slider. Just use an #EnvironmentObject.
Don't forget environment objects must be in the ancestor chain (you can't unfortunately go "sideways").
EnvironmentObject is only for parent-child chains.
Somewhat confusingly, you can't use the simple EnvironmentObject system if the items in question are in the same "environment!" EnvironmentObject should perhaps be named something like "ParentChainObject" or "NavigationViewChainObject".
EnvironmentObject is only used when you are using NavigationView.
import SwiftUI
import PlaygroundSupport
// using ancestor views ...
class SliderValue: ObservableObject {
#Published var position: Double = 11.0
}
struct HandySliderPopUp: View {
#EnvironmentObject var sv: SliderValue
var body: some View {
Slider(value: $sv.position, in: 0...10)
}
}
struct ContentView: View {
#StateObject var sliderValue = SliderValue()
var body: some View {
NavigationView {
VStack{
NavigationLink(destination:
HandySliderPopUp().frame(width: 400)) {
Text("click me")
}
Text(String(sliderValue.position))
}
}
.environmentObject(sliderValue) //HERE
}
}
PlaygroundPage.current.setLiveView(ContentView())
Note that //HERE is where you "set" the environment object.
For the "usual" situation, where it's the "same" view, see other answer.
Late to the party, this is what I did:
struct DoubleSlider: View {
#State var value: Double
let range: ClosedRange<Double>
let step: Double
let onChange: (Double) -> Void
init(initialValue: Double, range: ClosedRange<Double>, step: Double, onChange: #escaping (Double) -> Void) {
self.value = initialValue
self.range = range
self.step = step
self.onChange = onChange
}
var body: some View {
let binding = Binding<Double> {
return value
} set: { newValue in
value = newValue
onChange(newValue)
}
Slider(value: binding, in: range, step: step)
}
}
Usage:
DoubleSlider(initialValue: state.tipRate, range: 0...0.3, step: 0.01) { rate in
viewModel.state.tipRate = rate
}
Just use the onEditingChanged parameter of Slider. The argument is true while the user is moving the slider or still in contact with it. I do my updates when the argument changes from true to false.
struct MyView: View {
#State private var value = 0.5
func update(changing: Bool) -> Void {
// Do whatever
}
var body: some View {
Slider(value: $value, onEditingChanged: {changing in self.update(changing) })
{ pressed in }
}
}

Resources