SwiftUI - How do I change a synchronous value using an asynchronous operation inside a List Cell - swift

I have this previous question properly answer. That case is of an image somewhere in the interface.
I have another variation of the same problem but now the image is inside a List Cell.
That image shows a padlock that must only show if a particular inapp purchase was not purchased, on SwiftUI.
Something like
Image(systemName: "lock.circle.fill")
.renderingMode(.template)
.foregroundColor(.white)
.font(symbolFont)
.opacity(wasPurchased(item: item))
But as far as I see wasPurchased must be a synchronous function, right?
Something like
func wasPurchased(item: item) -> Bool {
return check(item:item) ? true : false
}
But, such checks normally happen asynchronously, over the network, and the function, as I see, must have a signature like
func wasPurchased(item: item, runOnFinishChecking:(Bool)->()) {
That list is populated from Core Data, like this
#FetchRequest(fetchRequest: Expressao.getAllItemsRequest())
private var allItems: FetchedResults<Expressao>
var body: some View {
List {
ForEach(allItems, id: \.self) { item in
HStack {
Text(item.term)
.font(fontItems)
.foregroundColor(.white)
Image(systemName: "lock.circle.fill")
.renderingMode(.template)
.foregroundColor(.white)
.font(symbolFont)
.opacity(wasPurchased(item: item))
}
}
}
}
I don't see how I can use something asynchronous to control the opacity of such element when the whole thing is an array.
How do I do that?

Just separate your row content into standalone view and apply approach from previous post.
var body: some View {
List {
ForEach(allItems, id: \.self) {
ExpressaoRowView(item: $0)
}
}
}
...
struct ExpressaoRowView: View {
#ObservedObject var item: Expressao
#State private var locked = true
var body: some View {
HStack {
Text(item.term)
.font(fontItems)
.foregroundColor(.white)
Image(systemName: "lock.circle.fill")
.renderingMode(.template)
.foregroundColor(.white)
.font(symbolFont)
.opacity(self.locked ? 1 : 0)
}
.onAppear {
self.wasPurchased(self.item) { purchased in
DispatchQueue.main.async {
self.locked = !purchased
}
}
}
}
}
Note: you can even keep wasPurchased checker somewhere outside (in parent or in some helper) and inject into ExpressaoRowView as a property

Related

is it possible get List array to load horizontally in swiftUI?

Do I need to dump using List and just load content into a Scrollview/HStack or is there a horizontal equivalent to stack? I would like to avoid having to set it up differently, but am willing todo so if there is no alternative... it just means recoding multiple other views.
current code for perspective:
import SwiftUI
import Combine
struct VideoList: View {
#Environment(\.presentationMode) private var presentationMode
#ObservedObject private(set) var viewModel: ViewModel
#State private var isRefreshing = false
var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("Home") // set image here
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
}
}
}
var body: some View {
NavigationView {
List(viewModel.videos.sorted { $0.id > $1.id}, id: \.id) { video in
NavigationLink(
destination: VideoDetails(viewModel: VideoDetails.ViewModel(video: video))) {
VideoRow(video: video)
}
}
.onPullToRefresh(isRefreshing: $isRefreshing, perform: {
self.viewModel.fetchVideos()
})
.onReceive(viewModel.$videos, perform: { _ in
self.isRefreshing = false
})
}
.onAppear(perform: viewModel.fetchVideos)
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
In general, List is List and it by design is vertical-only. For all horizontal case we should use ScrollView+HStack or ScrollView+LazyHStack (SwiftUI 2.0).
Anyway here is a simple demo of possible way that can be applicable in some particular cases. Prepared & tested with Xcode 12 / iOS 14.
Note: all tuning and alignments fixes are out of scope - only possibility demo.
struct TestHorizontalList: View {
let data = Array(1...20)
var body: some View {
GeometryReader { gp in
List {
ForEach(data, id: \.self) {
RowDataView(item: $0)
.rotationEffect(.init(degrees: 90)) // << rotate content back
}
}
.frame(height: gp.size.width) // initial fit in screen
.rotationEffect(.init(degrees: -90)) // << rotate List
}
}
}
struct RowDataView: View {
let item: Int
var body: some View {
RoundedRectangle(cornerRadius: 25.0).fill(Color.blue)
.frame(width: 80, height: 80)
.overlay(
Text("\(item)")
)
}
}

SwiftUI 2 Observable objects in one view which depend on each other

Thanks for all the support I have received, I trying to build an macos app that tags pdfs for machine learning purposes. I have followed Stanford SwiftUI course, and I want to create main view for my app that contains the document and to type a regex string to find in the document. The deal is I need to create a document chooser, to add documents to be analized, but I don't know how to deal with 2 view models in the same view. In fact one of those view models depend on the other one. The solution I found (not a solution a messy workaround) is to initialize the document manager as a separate view and use it as a navigation view, with a navigation link, but the look is horrible. I'll paste the code and explain it better.
This is the stores view
struct PDFTaggerDocumentStoreView: View {
#EnvironmentObject var store:PDFTaggerDocumentStore
var body: some View {
NavigationView {
List {
Spacer()
Text("Document Store").fontWeight(.heavy)
Button(action: {self.store.addDocument()}, label: {Text("Add document")})
Divider()
ForEach(store.documents){ doc in
NavigationLink(destination: PDFTaggerMainView(pdfTaggerDocument: doc)) {
Text(self.store.name(for: doc))
}
}
.onDelete { indexSet in
indexSet.map{self.store.documents[$0]}.forEach { (document) in
self.store.removeDocuments(document)
}
}
}
}
}
}
The main view.
struct PDFTaggerDocumentView: View {
#ObservedObject var document:PDFTaggerDocument
#State private var expression = ""
#State private var regexField = ""
#State private var showExpressionEditor = false
var body: some View {
VStack {
ZStack {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.stroke(Color.white, lineWidth: 0.5)
.frame(width: 600)
.padding()
VStack {
Text("Try expression ")
HStack {
TextField("Type regex", text: $document.regexString)
.frame(width: 200)
Image(nsImage: NSImage(named: "icons8-save-80")!)
.scaleEffect(0.3)
.onTapGesture {
self.showExpressionEditor = true
print(self.document.regexString)
print(self.regexField)
}
.popover(isPresented: $showExpressionEditor) {
ExpressionEditor().environmentObject(self.document)
.frame(width: 200, height: 300)
}
}
Picker(selection: $expression, label: EmptyView()) {
ForEach(document.expressionNames.sorted(by: >), id:\.key) { key, value in
Text(key)
}
}
Button(action: self.addToDocument, label: {Text("Add to document")})
.padding()
.frame(width: 200)
}
.frame(width:600)
.padding()
}
.padding()
Rectangle()
.foregroundColor(Color.white).overlay(OptionalPDFView(pdfDocument: document.backgroundPDF))
.frame(width:600, height:500)
.onDrop(of: ["public.file-url"], isTargeted: nil) { (providers, location) -> Bool in
let result = providers.first?.hasItemConformingToTypeIdentifier("public.file-url")
providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url") { data, error in
if let safeData = data {
let newURL = URL(dataRepresentation: safeData, relativeTo: nil)
DispatchQueue.main.async {
self.document.backgroundURL = newURL
}
}
}
return result!
}
}
}
I'd like to be able to initialize both views models in the same view, and make the document view, be dependent on the document chooser model.
Is there a way I can do it?
Thanks a lot for your time.

Disable Scrolling in SwiftUI List/Form

Lately, I have been working on creating a complex view that allows me to use a Picker below a Form. In every case, the Form will only have two options, thus not enough data to scroll downwards for more data. Being able to scroll this form but not Picker below makes the view feel bad. I can't place the picker inside of the form or else SwiftUI changes the styling on the Picker. And I can't find anywhere whether it is possible to disable scrolling on a List/Form without using:
.disable(condition)
Is there any way to disable scrolling on a List or Form without using the above statement?
Here is my code for reference
VStack{
Form {
Section{
Toggle(isOn: $uNotifs.notificationsEnabled) {
Text("Notifications")
}
}
if(uNotifs.notificationsEnabled){
Section {
Toggle(isOn: $uNotifs.smartNotifications) {
Text("Enable Smart Notifications")
}
}.animation(.easeInOut)
}
} // End Form
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
if(!uNotifs.smartNotifications){
GeometryReader{geometry in
HStack{
Picker("",selection: self.$hours){
ForEach(0..<24){
Text("\($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:geometry.size.width / CGFloat(5))
.clipped()
Text("hours")
Picker("",selection: self.$min){
ForEach(0..<61){
Text("\($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:geometry.size.width / CGFloat(5))
.clipped()
Text("min")
}
Here it is
Using approach from my post SwiftUI: How to scroll List programmatically [solution]?, it is possible to add the following extension
extension ListScrollingProxy {
func disableScrolling(_ flag: Bool) {
scrollView?.isScrollEnabled = !flag
}
}
and the use it as in example for above demo
struct DemoDisablingScrolling: View {
private let scrollingProxy = ListScrollingProxy()
#State var scrollingDisabled = false
var body: some View {
VStack {
Button("Scrolling \(scrollingDisabled ? "Off" : "On")") {
self.scrollingDisabled.toggle()
self.scrollingProxy.disableScrolling(self.scrollingDisabled)
}
Divider()
List(0..<50, id: \.self) { i in
Text("Item \(i)")
.background(ListScrollingHelper(proxy: self.scrollingProxy))
}
}
}
}
You can use the .scrollDisabled(true) modifier on the component (Form or List) to accomplish this behavior.

Is there a way in SwiftUI to return a different view based on an optional binding having a value?

I am trying to show a view after fetching some data on my view model (where the data can be optional because the view model only fetches it on request).
Why is the following not possible / how should I go about it?
#Binding var someViewModel: SomeViewModel?
var body: some View {
if let viewModel = self.someViewModel {
return filledView(with: viewModel)
}
return emptyView()
}
The part not working here is an if let in the Swift UI view builder.
One solution would be to have a separate Bool that fires when data is loaded, or even an enum to identify when data is in and when it's not, but then the view code is full of optional value checking which isn't ideal.
E.g. I want to avoid doing something like:
Image(systemName: someViewModel?.icon.symbol() ?? "plus_sign")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor(Color.red)
.padding()
Any help would be greatly appreciated.
You could use map. It will unwrap the optional if it has some value or ignore it altogether:
var body: some View {
Group {
someViewModel.map { FilledView(with: $0) }
}
}
Just found another option, we could wrap both the empty and filled view function returns in an AnyView and the problems disappear as well.
Example of the empty would then become:
func emptyView() -> AnyView {
return AnyView(Text(""))
}
And example of the filled view becomes:
func filledView(for viewModel: SomeViewModel) -> AnyView {
return AnyView(VStack(alignment: .leading) {
Image(systemName: viewModel.icon.symbol())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.foregroundColor(Color.red)
.padding()
}
}
}

SwiftUI List disable cell press

I am using xCode 11 beta 7 with SwiftUI.
I have a simple list which each list element has several buttons. Currently when the user presses the cell(not the buttons) it is highlighting the back of the list cell(probably not the correct terminology for SwiftUI).
How do i disable this behaviour? I could not locate an obvious api to disable it.
List {
HStack {
Group {
Button(action: {}) {
Text("Read").padding(5)
}.onTapGesture {
print("1")
}
.padding()
.background(Color.blue)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Notify").padding(5)
}.onTapGesture {
print("2")
}
.padding()
.background(Color.purple)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Write").padding(5)
}.onTapGesture {
print("3")
}
.padding()
.background(Color.yellow)
.cornerRadius(5)
}
}
}
Same answer as in How to remove highlight on tap of List with SwiftUI?
I know I'm a bit late, but it's for those of you who are searching (like me 😇)
What I found
I guess you should take a look at the short article How to disable the overlay color for images inside Button and NavigationLink from #TwoStraws
Just add the .buttonStyle(PlainButtonStyle()) modifier to your item in the List and you'll have what you wanted. It also makes the Buttons work again in the List, which is another problem I encountered.
A working example for Swift 5.1 :
import Combine
import SwiftUI
struct YourItem: Identifiable {
let id = UUID()
let text: String
}
class YourDataSource: ObservableObject {
let willChange = PassthroughSubject<Void, Never>()
var items = [YourItem]()
init() {
items = [
YourItem(text: "Some text"),
YourItem(text: "Some other text")
]
}
}
struct YourItemView: View {
var item: YourItem
var body: some View {
VStack(alignment: .leading) {
Text(item.text)
HStack {
Button(action: {
print("Like")
}) {
Image(systemName: "heart.fill")
}
Button(action: {
print("Star")
}) {
Image(systemName: "star.fill")
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
struct YourListView: View {
#ObservedObject var dataSource = YourDataSource()
var body: some View {
List(dataSource.items) { item in
YourItemView(item: item)
}
.navigationBarTitle("List example", displayMode: .inline)
.edgesIgnoringSafeArea(.bottom)
}
}
#if DEBUG
struct YourListView_Previews: PreviewProvider {
static var previews: some View {
YourListView()
}
}
#endif
As said in the article, it also works with NavigationLinks. I hope it helped some of you 🤞🏻
This is my simplest solution that is working for me (lo and behold, I'm building my first app in Swift and SwiftUI as well as this being my first post on SO):
Wherever you have buttons, add .buttonStyle(BorderlessButtonStyle())
Button(action:{}){
Text("Hello")
}.buttonStyle(BorderlessButtonStyle())
Then on your list, add .onTapGesture {return}
List{
Text("Hello")
}.onTapGesture {return}
Instead of using a button try it with a gesture instead
Group {
Text("Notify").padding(5).gesture(TapGesture().onEnded() {
print("action2")
})
.padding()
.background(Color.purple)
.cornerRadius(5)
}