Where should ı assign variable in View - SwiftUI - swift

I want to assign my storeId, which I am getting from my API. I want to assign it to a global variable. I did this with using onAppear and it works, but it causes lag when the screen opens.
Im looking for better solution for this. Where should I assign the storeId to my global variable?
This is my code:
struct ContentView: View {
var body: some View {
ZStack {
NavigationView {
ScrollView {
LazyVStack {
ForEach(storeArray,id:\.id) { item in
if item.type == StoreItemType.store_Index.rawValue {
NavigationImageView(item: item, destinationView: ShowCaseView()
.navigationBarTitle("", displayMode: .inline)
.onAppear{
Config.storeId = item.data?.storeId
})
} else if item.type == StoreItemType.store_link.rawValue {
if item.data?.type == StoreDataType.html_Content.rawValue {
NavigationImageView(item: item, destinationView: WebView())
} else if item.data?.type == StoreDataType.product_List.rawValue {
NavigationImageView(item: item, destinationView: ProductListView())
} else if item.data?.type == StoreDataType.product_Detail.rawValue {
NavigationImageView(item: item, destinationView: ProductDetailView())
}
} else {
fatalError()
}
}
}
}
.navigationBarTitle("United Apps")
}
.onAppear {
if isOpened != true {
getStoreResponse()
}
}
ActivityIndicator(isAnimating: $isAnimating)
}
}
func getStoreResponse() {
DispatchQueue.main.async {
store.storeResponse.sink { (storeResponse) in
isAnimating = false
storeArray.append(contentsOf: storeResponse.items!)
isOpened = true
}.store(in: &cancellable)
store.getStoreResponse()
}
}
}
struct NavigationImageView <DestinationType : View> : View {
var item : Store
var destinationView: DestinationType
var body: some View {
NavigationLink(destination:destinationView ) {
Image(uiImage: (item.banner?.url)!.load())
.resizable()
.aspectRatio(CGFloat((item.banner?.ratio)!), contentMode: .fit)
.cornerRadius(12)
.shadow(radius: 4)
.frame(width: GFloat(UIScreen.main.bounds.width * 0.9),
height: CGFloat((UIScreen.main.bounds.width / CGFloat((item.banner?.ratio) ?? 1))))
}
}
}

Related

AnyView not re-rendering after state change

I have the func below that returns AnyView and should give different results based on device orientation. When the orientation is changed, it is detected and printed, but the view is not re-rendered. Any idea how to fix this?
func getView() -> AnyView {
#State var isPortrait = UIDevice.current.orientation.isPortrait
let a =
HStack {
if isPortrait {
VStack {
Text("text inside VStack")
}
} else {
HStack {
Text("text inside HStack")
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
print("notification isPortrait: \(UIDevice.current.orientation.isPortrait)")
isPortrait = UIDevice.current.orientation.isPortrait
}
.onAppear() {
isPortrait = UIDevice.current.orientation.isPortrait
}
return AnyView(a)
}
Here is my test code that shows the changing of orientations. Tested on ios 15, iPhone device.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var isPortrait = UIDevice.current.orientation.isPortrait // <--- here
var body: some View {
VStack (spacing: 40) {
Text("testing orientations")
// getView() // works just as well
myView // alternative without AnyView
}
}
var myView: some View {
Text(isPortrait ? "should be Portrait" : "should be Landscape")
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
isPortrait = UIDevice.current.orientation.isPortrait
}
.onAppear() {
isPortrait = UIDevice.current.orientation.isPortrait
}
}
func getView() -> AnyView {
let a =
HStack {
if isPortrait {
VStack {
Text("should be Portrait")
}
} else {
HStack {
Text("should be Landscape")
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
print("notification isPortrait: \(UIDevice.current.orientation.isPortrait)")
isPortrait = UIDevice.current.orientation.isPortrait
}
.onAppear() {
isPortrait = UIDevice.current.orientation.isPortrait
}
return AnyView(a)
}
}
EDIT1:
As mentioned by #Rob, the best way is to make a separate custom view, such as:
struct GetView: View {
#State var isPortrait = UIDevice.current.orientation.isPortrait
var body: some View {
VStack {
HStack {
if isPortrait {
VStack {
Text("should be Portrait")
}
} else {
HStack {
Text("should be Landscape")
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
print("notification isPortrait: \(UIDevice.current.orientation.isPortrait)")
isPortrait = UIDevice.current.orientation.isPortrait
}
.onAppear() {
isPortrait = UIDevice.current.orientation.isPortrait
}
}
}
}

Need help to make a customEditMode as Environment

I like to re-build a customEditMode like same editMode in SwiftUI for learning purpose, I could made my code until a full error codes as possible, that was not my plan! However here is what I tried until now, need help to get this codes work. thanks
Update:
Why this Circle color does not change?
struct ContentView: View {
#Environment(\.customEditMode) var customEditMode
var body: some View {
CircleView()
VStack {
Button("active") { customEditMode?.wrappedValue = CustomEditMode.active }.padding()
Button("inactive") { customEditMode?.wrappedValue = CustomEditMode.inactive }.padding()
Button("none") { customEditMode?.wrappedValue = CustomEditMode.none }.padding()
}
.onChange(of: customEditMode?.wrappedValue) { newValue in
if newValue == CustomEditMode.active {
print("customEditMode is active!")
}
else if newValue == CustomEditMode.inactive {
print("customEditMode is inactive!")
}
else if newValue == CustomEditMode.none {
print("customEditMode is none!")
}
}
}
}
struct CircleView: View {
#Environment(\.customEditMode) var customEditMode
var body: some View {
Circle()
.fill(customEditMode?.wrappedValue == CustomEditMode.active ? Color.green : Color.red)
.frame(width: 150, height: 150, alignment: .center)
}
}
If you take a closer look at the editMode:
#available(macOS, unavailable)
#available(watchOS, unavailable)
public var editMode: Binding<EditMode>?
you can see that it is in fact a Binding. That's why you access its value using wrappedValue.
You need to do the same for your CustomEditModeEnvironmentKey:
enum CustomEditMode {
case active, inactive, none
// optionally, if you need mapping to `Bool?`
var boolValue: Bool? {
switch self {
case .active: return true
case .inactive: return false
case .none: return nil
}
}
}
struct CustomEditModeEnvironmentKey: EnvironmentKey {
static let defaultValue: Binding<CustomEditMode>? = .constant(.none)
}
extension EnvironmentValues {
var customEditMode: Binding<CustomEditMode>? {
get { self[CustomEditModeEnvironmentKey] }
set { self[CustomEditModeEnvironmentKey] = newValue }
}
}
Here is a demo:
struct ContentView: View {
#State private var customEditMode = CustomEditMode.none
var body: some View {
TestView()
.environment(\.customEditMode, $customEditMode)
}
}
struct TestView: View {
#Environment(\.customEditMode) var customEditMode
var body: some View {
Circle()
.fill(customEditMode?.wrappedValue == .active ? Color.green : Color.red)
.frame(width: 150, height: 150, alignment: .center)
VStack {
Button("active") { customEditMode?.wrappedValue = .active }.padding()
Button("inactive") { customEditMode?.wrappedValue = .inactive }.padding()
Button("none") { customEditMode?.wrappedValue = .none }.padding()
}
.onChange(of: customEditMode?.wrappedValue) { newValue in
print(String(describing: newValue))
}
}
}

SwiftUI How to create LazyVStack with selection as List

I would like to have something like List(selection: ) in LazyVStack.
The problem is that I don't know how to manage the content to split in each element that it contains.
What I've tried to do:
public struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
let content: Content
var selection: Binding<Set<SelectionValue>>?
#Environment(\.editMode) var editMode
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content) {
self.content = content()
self.selection = selection
}
public var body: some View {
if self.editMode?.wrappedValue == EditMode.active {
HStack {
content //here I would like to have something like ForEach (content, id:\.self)
Button(action: {
//add the UUID to the list of selected item
}) {
Image(systemName: "checkmark.circle.fill")
//Image(systemName: selection?.wrappedValue.contains(<#T##member: Hashable##Hashable#>) ? "checkmark.circle.fill" : "circle")
}
}
}
else {
content
}
}
}
struct ListView: View {
#State private var editMode: EditMode = .inactive
#State private var selection = Set<UUID>()
#State private var allElements: [MyElement] = [MyElement(id: UUID(), text: "one"),
MyElement(id: UUID(), text: "two" ),
MyElement(id: UUID(), text: "tree" )
]
var body: some View {
NavigationView {
VStack {
Divider()
Text("LazyVStack")
.foregroundColor(.red)
LazyVStack {
ForEach(allElements, id: \.self) { element in //section data
Text(element.text)
}
}
Divider()
Text("LazyVStackSelectionable")
.foregroundColor(.red)
LazyVStackSelectionable(selection: $selection) {
ForEach(allElements, id: \.self) { element in //section data
Text(element.text)
}
}
Divider()
}
.environment(\.editMode, self.$editMode)
.navigationBarTitle(Text("LIST"), displayMode: .inline)
.navigationBarItems(//EDIT
trailing:
Group {
HStack (spacing: 15) {
self.editButton
self.delInfoButton
.contentShape(Rectangle())
}
}
)
}
}
//MARK: EDIT MODE
private func deleteItems() {
DispatchQueue.global(qos: .userInteractive).async {
Thread.current.name = #function
selection.forEach{ idToRemove in
if let index = allElements.firstIndex(where: { $0.id == idToRemove }) {
allElements.remove(at: index)
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var delInfoButton: some View {
if editMode == .inactive {
return Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
edit = .inactive
edit = .active
UPDATE
with Asperi's solution, I lose the propriety of LazyVStack, all the rows are loaded also if not displayed (and is also not scrollable:
struct SampleRow: View {
let number: Int
var body: some View {
Text("Sel Row \(number)")
}
init(_ number: Int) {
print("Loading LazySampleRow row \(number)")
self.number = number
}
}
struct LazySampleRow: View {
let number: Int
var body: some View {
Text("LVS element \(number)")
}
init(_ number: Int) {
print("Loading LazyVStack row \(number)")
self.number = number
}
}
var aLotOfElements: [MyElement] {
var temp: [MyElement] = []
for i in 1..<200 {
temp.append(MyElement(id: UUID(), number: i))
}
return temp
}
struct ContentView: View {
#State private var editMode: EditMode = .inactive
#State private var selection = Set<UUID>()
#State private var allElements: [MyElement] = aLotOfElements//[MyElement(id: UUID(), number: 1)]
var body: some View {
NavigationView {
HStack {
VStack {
Text("LazyVStack")
.foregroundColor(.red)
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(allElements, id: \.self) { element in //section data
LazySampleRow(element.number)
}
}
}
}
Divider()
VStack {
LazyVStack (alignment: .leading) {
Divider()
Text("LazyVStackSelectionable")
.foregroundColor(.red)
LazyVStackSelectionable(allElements, selection: $selection) { element in
SampleRow(element.number)
}
Divider()
}
}
}
.environment(\.editMode, self.$editMode)
.navigationBarTitle(Text("LIST"), displayMode: .inline)
.navigationBarItems(//EDIT
trailing:
Group {
HStack (spacing: 15) {
self.editButton
self.delInfoButton
.contentShape(Rectangle())
}
}
)
}
}
//MARK: EDIT MODE
private func deleteItems() {
DispatchQueue.global(qos: .userInteractive).async {
Thread.current.name = #function
selection.forEach{ idToRemove in
if let index = allElements.firstIndex(where: { $0.id == idToRemove }) {
allElements.remove(at: index)
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var delInfoButton: some View {
if editMode == .inactive {
return Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension EditMode {
var title: String {
self == .active ? NSLocalizedString("done", comment: "") : NSLocalizedString("edit", comment: "")
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}
You need to create custom handled containers for all variants of desired content types.
Below is a demo of possible direction on the example of following content support (by example of List)
LazyVStackSelectionable(allElements, selection: $selection) { element in
Text(element.text)
}
Demo prepared and tested with Xcode 12 / iOS 14 (it is used some SwiftUI 2.0 features so if needed SwiftUI 1.0 support some more tuning will be needed)
struct LazyVStackSelectionable<SelectionValue, Content> : View where SelectionValue : Hashable, Content : View {
#Environment(\.editMode) var editMode
private var selection: Binding<Set<SelectionValue>>?
private var content: () -> Content
private var editingView: AnyView?
init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: #escaping () -> Content)
{
self.selection = selection
self.content = content
}
var body: some View {
Group {
if editingView != nil && self.editMode?.wrappedValue == .active {
editingView!
} else {
self.content()
}}
}
}
extension LazyVStackSelectionable {
init<Data, RowContent>(_ data: Data, selection: Binding<Set<SelectionValue>>?, #ViewBuilder rowContent: #escaping (Data.Element) -> RowContent) where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable, SelectionValue == Data.Element.ID
{
self.init(selection: selection, content: {
ForEach(data) { el in
HStack {
rowContent(el)
}
}
})
editingView = AnyView(
ForEach(data) { el in
HStack {
rowContent(el)
if let selection = selection {
Button(action: {
if selection.wrappedValue.contains(el.id) {
selection.wrappedValue.remove(el.id)
} else {
selection.wrappedValue.insert(el.id)
}
}) {
Image(systemName: selection.wrappedValue.contains(el.id) ? "checkmark.circle.fill" : "circle")
}
}
}
}
)
}
}
Instead of creating custom LazyVStack I suggest to modify ContentView and pass bindings to it.
struct SampleRow: View {
let element: MyElement
let editMode: Binding<EditMode>
let selection: Binding<Set<UUID>>?
var body: some View {
HStack {
if editMode.wrappedValue == .active,
let selection = selection {
Button(action: {
if selection.wrappedValue.contains(element.id) {
selection.wrappedValue.remove(element.id)
} else {
selection.wrappedValue.insert(element.id)
}
}) {
Image(systemName: selection.wrappedValue.contains(element.id) ? "checkmark.circle.fill" : "circle")
}
}
Text("Sel Row \(element.number)")
}
}
init(_ element: MyElement,
editMode: Binding<EditMode>,
selection: Binding<Set<UUID>>?) {
print("Loading LazySampleRow row \(element.number)")
self.editMode = editMode
self.element = element
self.selection = selection
}
}
And then you can just wrap normal LazyVStack in ScrollView to achieve what you need.
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(allElements, id: \.self) {
SampleRow($0,
editMode: $editMode,
selection: $selection)
}
}
}

How to present modal in SwiftUI automatically without button

I am thinking of using a switch case to present different views.
struct searchview : View {
var body: some View {
VStack {
VStack {
if self.speechRecognition.isPlaying == true {
VStack {
Text(self.speechRecognition.recognizedText).bold()
.foregroundColor(.white)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.font(.system(size: 50))
.sheet(isPresented: $showSheet) {
self.sheetView
}
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showSheet = true
}
}
}
}.onAppear(perform: getViews)
}
var currentSheetView: String {
var isProductDictEmpty = Global.productDict.isEmpty
var wasTextRecognizedEmpty = self.speechRecognition.recognizedText.isEmpty
var checkTextandDict = (wasTextRecognizedEmpty,isProductDictEmpty)
switch checkTextandDict {
case (true,true):
print("Product dict and Text rec are empty")
return "error"
case (false,false):
print("Yes we are in business")
return "product"
case (true,false):
print("OOPS we didnt catch that")
return "error"
case (false,true):
print("OOPS we didnt catch that")
return "zero match"
}
}
#ViewBuilder
var sheetView: some View {
if currentSheetView == "product" {
ProductSearchView(model: self.searchModel)
}
else if currentSheetView == "zero match" {
zeroResult()
}
else if currentSheetView == "error" {
SearchErrorView()
}
}
}
}
I know how to use .sheet modifier to present modal when a button is pressed but how could I the present the respective modals in swift cases automatically with button?
UPDATE
these are the views I am trying to implement
struct SearchErrorView: View {
var body: some View {
VStack {
Text("Error!")
Text("Oops we didn't catch that")
}
}
}
struct zeroResult: View {
var body: some View {
Text("Sorry we did not find any result for this item")
}
}
I'm not sure what I am doing wrongly. I tried to implement the solution below but still not able to call the views with the switch cases.
The solution is to programmatically set a variable controlling displaying of your sheet.
You can try the following to present your sheet in onAppear:
struct ContentView: View {
#State var showSheet = false
var body: some View {
Text("Main view")
.sheet(isPresented: $showSheet) {
self.sheetView
}
.onAppear {
self.showSheet = true
}
}
var currentSheetView: String {
"view1" // or any other...
}
#ViewBuilder
var sheetView: some View {
if currentSheetView == "view1" {
Text("View 1")
} else {
Text("View 2")
}
}
}
You may delay it as well:
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showSheet = true
}
}
Just setting self.showSheet = true will present the sheet. It's up to you how you want to trigger it.

Multiple filters/toggles for list in SwiftUI

I have made a dynamic list in SWiftUI, and I have inserted two toggles to filter the list. However, it isn't very straightforward how to make both toggles affect the same list, since the toggle brackets around {} the list.
I have tried to make the second toggle wrap around the first, but that doesnt work.
HStack {
Toggle(isOn: $hideReds) {
Text("Hide reds")
.foregroundColor(.red)
}
.padding(.horizontal, 30)
Toggle(isOn: $hideWhitess) {
Text("Hide whites")
.foregroundColor(.blue)
} .padding(.horizontal, 22)
}
List {
Section {
ForEach(AGrapes) { grape in
if !self.hideReds || grape.isRed {
GrapeCell(grape: grape)
}
}
.onDelete(perform: delete)
.onMove(perform: move)
}
}
I thought I needed to add:
if !self.hideWhites || grape.isWhite {}
but where??
You can filter before passing the list like so.
var filteredGrapes: [Grape] {
return AGrapes.filter({ (grape) -> Bool in
return !((self.hideReds && grape.isRed) || (self.hideWhitess && grape.isWhite))
})
}
var body: some View {
VStack{
HStack {
Toggle(isOn: $hideReds) {
Text("Hide reds")
.foregroundColor(.red)
}
.padding(.horizontal, CGFloat(30))
Toggle(isOn: $hideWhitess) {
Text("Hide whites")
.foregroundColor(.blue)
} .padding(.horizontal, 22)
}
List {
Section {
ForEach(filteredGrapes) { grape in
GrapeCell(grape: grape)
}
.onDelete(perform: delete)
.onMove(perform: move)
}
}
}
}
You should use a filter:
import SwiftUI
struct Grape: Identifiable {
let id = UUID()
let name: String
let isRed: Bool
}
struct ContentView: View {
#State private var AGrapes: [Grape] = [Grape(name: "Merlot", isRed: true), Grape(name: "Cabernet Sauvignon", isRed: true), Grape(name: "Pinot Noir", isRed: true), Grape(name: "Albariño", isRed: false), Grape(name: "Sauvignon Blanc", isRed: false)]
#State private var hideReds = false
#State private var hideWhitess = false
var body: some View {
VStack {
HStack {
Toggle(isOn: $hideReds) {
Text("Hide reds").foregroundColor(.red)
}
.padding(.horizontal, 30)
Toggle(isOn: $hideWhitess) {
Text("Hide whites").foregroundColor(.blue)
}.padding(.horizontal, 22)
}
List {
Section {
ForEach(AGrapes.filter({ return (!self.hideWhitess && !$0.isRed) || (!self.hideReds && $0.isRed) })) { grape in GrapeCell(grape: grape)
}
}
}
}
}
}
struct GrapeCell: View {
let grape: Grape
var body: some View {
HStack {
Circle().fill(grape.isRed ? Color.red : Color.green).frame(width: 30)
Text(grape.name)
Spacer()
}
}
}