Need help to make a customEditMode as Environment - swift

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

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

How to setup NavigationLink in SwiftUI sheet to redirect to new view

I am attempting to build a multifaceted openweathermap app. My app is designed to prompt the user to input a city name on a WelcomeView, in order to get weather data for that city. After clicking search, the user is redirected to a sheet with destination: DetailView, which displays weather details about that requested city. My goal is to disable dismissal of the sheet in WelcomeView and instead add a navigationlink to the sheet that redirects to the ContentView. The ContentView in turn is set up to display a list of the user's recent searches (also in the form of navigation links).
My issues are the following:
The navigationLink in the WelcomeView sheet does not work. It appears to be disabled. How can I configure the navigationLink to segue to destination: ContentView() ?
After clicking the navigationLink and redirecting to ContentView, I want to ensure that the city name entered in the WelcomeView textfield is rendered as a list item in the ContentView. For that to work, would it be necessary to set up an action in NavigationLink to call viewModel.fetchWeather(for: cityName)?
Here is my code:
WelcomeView
struct WelcomeView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State private var showingDetail: Bool = false
#State private var linkActive: Bool = true
#State private var acceptedTerms = false
var body: some View {
Section {
HStack {
TextField("Search Weather by City", text: $cityName)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding()
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
VStack {
NavigationLink(destination: ContentView()){
Text("Return to Search")
}
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}.interactiveDismissDisabled(!acceptedTerms)
}
}
}.padding()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
ContentView
let coloredToolbarAppearance = UIToolbarAppearance()
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State var showingDetail = false
init() {
// toolbar attributes
coloredToolbarAppearance.configureWithOpaqueBackground()
coloredToolbarAppearance.backgroundColor = .systemGray5
UIToolbar.appearance().standardAppearance = coloredToolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = coloredToolbarAppearance
}
var body: some View {
NavigationView {
VStack() {
List () {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
}
}
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.onAppear() {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DemoApp
#main
struct SwftUIMVVMWeatherDemoApp: App {
var body: some Scene {
WindowGroup {
// ContentView()
WelcomeView()
}
}
}

onTapGesture fails whole appliaction

I am making a little game with some youtube tutorials, I made the design but whenever I try to add onTapGesture my xCode returns an error Cannot Preview This File, and the simulator launches but it's just all white
struct ContentView: View {
var body: some View {
HStack{
CardView()
CardView()
CardView()
CardView()
}
.foregroundColor(.orange)
.padding(.horizontal)
}
}
struct CardView:View {
#State var isFaceUp:Bool = false
var body: some View {
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp {
shape.stroke(lineWidth: 3)
Text("🧞‍♂️").font(.largeTitle)
} else{
shape.fill()
}
}
onTapGesture {
isFaceUp = !isFaceUp
}
}
}
Correct the syntax and it'll fix the crash:
struct CardView:View {
#State var isFaceUp:Bool = false
var body: some View {
ZStack{
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp {
shape.stroke(lineWidth: 3)
Text("🧞‍♂️").font(.largeTitle)
} else{
shape.fill()
}
}.onTapGesture {
isFaceUp = !isFaceUp
}
}
}

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

Background of button not conditionally rendering

Essentially I have a button when pressed I want the background to become a different color. In order to do this I have an object that I alter, I have printed out the value of the Bool value in the object and see its changing but the color of the button is not changing.
Object With Bool:
class dummyObject: Identifiable, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var id = UUID()
var isSelected: Bool {
willSet {
objectWillChange.send()
}
}
init(isSelected:Bool) {
self.isSelected = isSelected
}
}
View:
struct SelectionView: View {
var objs: [dummyObject] = [
dummyObject.init(isSelected: false)
]
var body: some View {
HStack{
ForEach(objs) { obj in
Button(action: {
obj.isSelected.toggle()
print("\(obj.isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(obj.isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(obj.isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}
Extract your Button into other view, where obj is #ObservedObject and everything will work:
import SwiftUI
import Combine
class dummyObject: Identifiable, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var id = UUID()
var isSelected: Bool {
willSet {
objectWillChange.send()
}
}
init(isSelected:Bool) {
self.isSelected = isSelected
}
}
struct SelectionView: View {
var objs: [dummyObject] = [dummyObject.init(isSelected: false)]
var body: some View {
HStack{
ForEach(objs) { obj in
ObjectButton(obj: obj)
}
}
}
}
struct ObjectButton: View {
#ObservedObject var obj: dummyObject
var body: some View {
Button(action: {
self.obj.isSelected.toggle()
print("\(self.obj.isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(obj.isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(obj.isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}
struct SelectionView_Previews: PreviewProvider {
static var previews: some View {
SelectionView()
}
}
Here is modified your snapshot of code that works. Tested with Xcode 11.2 / iOS 13.2.
The main idea is made a model as value-type, so modifications of properties modify model itself, and introducing #State for view would refresh on changes.
struct dummyObject: Identifiable, Hashable {
var id = UUID()
var isSelected: Bool
}
struct SelectionView: View {
#State var objs: [dummyObject] = [
dummyObject(isSelected: false)
]
var body: some View {
HStack{
ForEach(Array(objs.enumerated()), id: \.element) { (i, _) in
Button(action: {
self.objs[i].isSelected.toggle()
print("\(self.objs[i].isSelected)")
}) {
VStack {
Text("Test")
.foregroundColor(self.objs[i].isSelected ? Color.white : Color.gray)
.font(.caption)
}
}.frame(width:55,height: 55)
.padding()
.background(self.objs[i].isSelected ? Color.red : Color.white)
.padding(.horizontal, 3)
.clipShape(Circle()).shadow(radius: 6)
}
}.frame(minWidth: 0, maxWidth: .infinity)
.padding()
}
}