AnyView not re-rendering after state change - swift

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
}
}
}
}

Related

SwiftUI: Unable to access Edit Mode within TabView

I currently have a view HostView that provides HostSummaryView and EditHostSummaryView, where the former responds to editMode.wrappedValue? == .inactive. HostView looks like this:
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
HStack {
EditButton()
}
if editMode?.wrappedValue == .inactive {
HostSummary()
} else {
EditHostSummary()
}
}
}
I have a RootView that contains a TabView, which looks like this:
struct RootView: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
View1()
.onTapGesture { self.selectedTab = 0 }
.tag(0)
View2()
.tag(1)
HostView()
.tag(2)
}
}
}
I tried passing the #Environment(\.editMode) var editMode to HostView, but that did not fix the problem. The EditButton does not toggle editMode in the HostView. However, HostView works when I access it through a non-TabView view.
How can I get this to work?
I couldn't find a previous question about this but it is known that some things don't work from within a TabView you have to push it down a View.
I think it is considered a bug.
struct EditableHost: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
Text("View 1")
.onTapGesture { self.selectedTab = 0 }
.tag(0)
Text("View 2")
.tag(1)
ParentHostView()
.tabItem { Text("host") }
.tag(2)
}
}
}
struct ParentHostView: View {
#State var active: Bool = true
var body: some View {
NavigationView{
NavigationLink(
destination: HostView(),
isActive: $active,
label: {
Text("HOST")
})
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
VStack{
HStack {
EditButton()
}
if editMode?.wrappedValue == .inactive {
Text("HostSummary")
} else {
Text("EditHostSummary")
}
}.navigationBarBackButtonHidden(true)
}
}
I encounter the same case(Xcode 13.3, iOS 15.4). You can customize an edit button and everything should be ok.
struct HostView: View {
#Environment(\.editMode) var editMode
var body: some View {
HStack {
Button {
if editMode != nil{
if editMode!.wrappedValue == .inactive{
editMode!.wrappedValue = .active
}else{
if editMode!.wrappedValue == .active{
editMode!.wrappedValue = .inactive
}
}
}
} label: {
Text(editMode?.wrappedValue.isEditing == true ? "Done" : "Edit")
}
}
if editMode?.wrappedValue == .inactive {
HostSummary()
} else {
EditHostSummary()
}
}
}

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))
}
}
}

Where should ı assign variable in View - SwiftUI

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))))
}
}
}

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.

SwiftUI - Hiding a ScrollView's indicators makes it stop scrolling

I'm trying to hide the indicators of a ScrollView but when I try doing so, the ScrollView just doesn't scroll anymore. I'm using macOS if that matters.
ScrollView(showsIndicators: false) {
// Everything is in here
}
On request of #SoOverIt
Demo:
Nothing special, just launched some other test example. Xcode 11.2 / macOS 10.15
var body : some View {
VStack {
ScrollView([.vertical], showsIndicators: false) {
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
Group {
Text("AAA")
Text("BBB")
Text("CCC")
Text("DDD")
Text("EEE")
}
}
.frame(height: 100)
.border(Color.blue)
}
.border(Color.red)
}
I fixed the issue.
extension View {
func hideIndicators() -> some View {
return PanelScrollView{ self }
}
}
struct PanelScrollView<Content> : View where Content : View {
let content: () -> Content
var body: some View {
PanelScrollViewControllerRepresentable(content: self.content())
}
}
struct PanelScrollViewControllerRepresentable<Content>: NSViewControllerRepresentable where Content: View{
func makeNSViewController(context: Context) -> PanelScrollViewHostingController<Content> {
return PanelScrollViewHostingController(rootView: self.content)
}
func updateNSViewController(_ nsViewController: PanelScrollViewHostingController<Content>, context: Context) {
}
typealias NSViewControllerType = PanelScrollViewHostingController<Content>
let content: Content
}
class PanelScrollViewHostingController<Content>: NSHostingController<Content> where Content : View {
var scrollView: NSScrollView?
override func viewDidAppear() {
self.scrollView = findNSScrollView(view: self.view)
self.scrollView?.scrollerStyle = .overlay
self.scrollView?.hasVerticalScroller = false
self.scrollView?.hasHorizontalScroller = false
super.viewDidAppear()
}
func findNSScrollView(view: NSView?) -> NSScrollView? {
if view?.isKind(of: NSScrollView.self) ?? false {
return (view as? NSScrollView)
}
for v in view?.subviews ?? [] {
if let vc = findNSScrollView(view: v) {
return vc
}
}
return nil
}
}
Preview:
struct MyScrollView_Previews: PreviewProvider {
static var previews: some View {
ScrollView{
VStack{
Text("hello")
Text("hello")
Text("hello")
Text("hello")
Text("hello")
}
}.hideIndicators()
}
}
So... I think that's the only way for now.
You basically just put a View over your ScrollView indicator with the same backgroundColor as your background View
Note: This obviously only works if your background is static with no content at the trailing edge.
Idea
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme: ColorScheme
let yourBackgroundColorLight: Color = .white
let yourBackgroundColorDark: Color = .black
var yourBackgroundColor: Color { colorScheme == .light ? yourBackgroundColorLight : yourBackgroundColorDark }
var body: some View {
ScrollView {
VStack {
ForEach(0..<1000) { i in
Text(String(i)).frame(width: 280).foregroundColor(.green)
}
}
}
.background(yourBackgroundColor) //<-- Same
.overlay(
HStack {
Spacer()
Rectangle()
.frame(width: 10)
.foregroundColor(yourBackgroundColor) //<-- Same
}
)
}
}
Compact version
You could improve this like that, I suppose you have your color dynamically set up inside assets.
Usage:
ScrollView {
...
}
.hideIndicators(with: <#Your Color#>)
Implementation:
extension View {
func hideIndicators(with color: Color) -> some View {
return modifier(HideIndicators(color: color))
}
}
struct HideIndicators: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.overlay(
HStack {
Spacer()
Rectangle()
.frame(width: 10)
.foregroundColor(color)
}
)
}
}