I am creating a SwiftUI app and wanted to add a settings screen that would be displayed as a modal sheet when tapping on a gear icon. For some reason when tapping on the gear when the app is running on a device or the simulator, the sheet does not appear. However, it does appear on the Xcode Preview when I run it.
I have included the entire ContentView code below and highlighted the part with the button that is supposed to display the sheet. SettingsPage() is just the normal SwiftUI template that says Hello, World.
Thanks.
struct ContentView: View {
#StateObject var locationViewModel = LocationManager()
#State private var showMoreInfo: Bool = false
#State private var showingTopSpeed: Bool = false
#State var showingSettingsScreen: Bool = false
private var currentSpeed: String = "Your current speed is:"
private var topSpeed: String = " Your top speed is:"
var body: some View {
VStack {
HStack(alignment: .lastTextBaseline) {
GPSStrengthView(horizontalAccuracy: locationViewModel.userHorizontalAccuracy)
Spacer()
Button(action: {
withAnimation {
showingTopSpeed.toggle()
print("Toggling showing top speed")
}
}) {
Image(systemName: "speedometer")
.foregroundColor(showingTopSpeed ? .primary : .accentColor)
}
Button(action: {
withAnimation {
showMoreInfo.toggle()
}
}) {
Image(systemName: "filemenu.and.selection")
.renderingMode(.original)
}
//BUTTON IN QUESTION ####################################
Button(action: {
showingSettingsScreen = true
}) {
Image(systemName: "gear")
} .sheet(isPresented: $showingSettingsScreen, content: {
SettingsPage()
})
//############################################
}
.padding(.leading)
.padding(.trailing)
Spacer()
Text(showingTopSpeed ? currentSpeed : topSpeed)
SpeedView(speed: showingTopSpeed ? locationViewModel.userSpeed : locationViewModel.userTopSpeed)
if showMoreInfo {
Text(String(format: "With an accuracy of: +/- %.2f", locationViewModel.userSpeedAccuracy * 2.236936))
.animation(.easeInOut)
}
Spacer()
HStack {
Spacer()
CompassView()
.scaleEffect(scaleVar)
.frame(width: frameDimensions, height: frameDimensions)
}
.padding()
}
}
Try moving your .sheet() to the outermost VStack instead of applying it directly to the Button
struct ContentView: View {
#StateObject var locationViewModel = LocationManager()
#State private var showMoreInfo: Bool = false
#State private var showingTopSpeed: Bool = false
#State var showingSettingsScreen: Bool = false
private var currentSpeed: String = "Your current speed is:"
private var topSpeed: String = " Your top speed is:"
var body: some View {
VStack {
HStack(alignment: .lastTextBaseline) {
GPSStrengthView(horizontalAccuracy: locationViewModel.userHorizontalAccuracy)
Spacer()
Button(action: {
withAnimation {
showingTopSpeed.toggle()
print("Toggling showing top speed")
}
}) {
Image(systemName: "speedometer")
.foregroundColor(showingTopSpeed ? .primary : .accentColor)
}
Button(action: {
withAnimation {
showMoreInfo.toggle()
}
}) {
Image(systemName: "filemenu.and.selection")
.renderingMode(.original)
}
//BUTTON IN QUESTION ####################################
Button(action: {
showingSettingsScreen = true
}) {
Image(systemName: "gear")
}
//############################################
}
.padding(.leading)
.padding(.trailing)
Spacer()
Text(showingTopSpeed ? currentSpeed : topSpeed)
SpeedView(speed: showingTopSpeed ? locationViewModel.userSpeed : locationViewModel.userTopSpeed)
if showMoreInfo {
Text(String(format: "With an accuracy of: +/- %.2f", locationViewModel.userSpeedAccuracy * 2.236936))
.animation(.easeInOut)
}
Spacer()
HStack {
Spacer()
CompassView()
.scaleEffect(scaleVar)
.frame(width: frameDimensions, height: frameDimensions)
}
.padding()
}
.sheet(isPresented: $showingSettingsScreen, content: {
SettingsPage()
})
}
Related
How can I add a button next to the searchbar when it is currently selected in SwiftUI? An example of what I am talking about is below:
This is the code I have currently:
struct ContentView: View {
#State private var searchText = ""
var body: some View {
NavigationView {
VStack {
Text("Hello")
}
.searchable(text: $searchText)
.navigationTitle("Hello")
}
}
}
If this is not possible to do in SwiftUI, is there a way I can accomplish this by utilizing UIKit in my SwiftUI view?
You can achieve this very easily with #FocusState. here is complete code, you can change it according to your requirements.
struct CustomSearchBar: View {
#Binding var text: String
#State private var isEditing = false
#FocusState private var isSearchFocused: Bool
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.white)
.padding([.leading])
TextField("Search ...", text: $text)
.padding([.trailing,. top, .bottom],7)
.foregroundColor(.white)
.opacity(isEditing ? 1 : 0.7)
.focused($isSearchFocused)
if isSearchFocused {
Button(action: {
// do you option event
}) {
Image("option")
.resizable()
.frame(width: 25, height: 25)
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
}
Button(action: {
// do you cancle event
isSearchFocused = false
}) {
Text("Cancel")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
}
.padding()
.background(.gray.opacity(0.3))
.onChange(of: isSearchFocused) { newValue in
text = ""
}
}
}
I have been following a tutorial on creating a weather app. I am trying to take it further. When on the weather view the user can click a plus button which takes them to a location view. Here the user will be able to update the location then when going back the weather will reload. But i am really struggling to get the weather to reload when pressing the back button.
Below is my code for the WeatherView & LocationView
Thank you
struct WeatherView: View {
#State private var isShowing = false
#State var width = UIScreen.main.bounds.width - 60
#State var x = -UIScreen.main.bounds.width + 60
#ObservedObject var input = CityId()
let heptics = UIImpactFeedbackGenerator(style: .medium)
#ObservedObject var weatherViewModel = WeatherViewModel()
var body: some View {
NavigationView{
ZStack {
BackgroundView()
VStack {
if weatherViewModel.stateView == .loading {
ActivityIndicatorView(isAnimating: true).configure {
$0.color = .white
}
}
if weatherViewModel.stateView == .success {
LocationAndTemperatureHeaderView(data: weatherViewModel.currentWeather)
Spacer()
ScrollView(.vertical, showsIndicators: false) {
VStack {
DailyWeatherCellView(data: weatherViewModel.todayWeather)
Rectangle().frame(height: CGFloat(1))
HourlyWeatherView(data: weatherViewModel.hourlyWeathers)
Rectangle().frame(height: CGFloat(1))
DailyWeatherView(data: weatherViewModel.dailyWeathers)
Rectangle().frame(height: CGFloat(1))
Text(weatherViewModel.currentDescription)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(
.init(arrayLiteral:.leading,.trailing),
24
)
Rectangle().frame(height: CGFloat(1))
DetailsCurrentWeatherView(data: weatherViewModel.currentWeather)
Rectangle().frame(height: CGFloat(1))
}
}
Spacer()
}
if weatherViewModel.stateView == .failed {
Button(action: {
self.weatherViewModel.retry()
}) {
Text("Failed get data, retry?")
.foregroundColor(.white)
}
}
}
}.colorScheme(.dark)
.listStyle(InsetGroupedListStyle())
.background(
NavigationLink(destination: LocationView(input: input, weatherViewModel: weatherViewModel), isActive: $isShowing) {
EmptyView()
}
) // End of Background
.navigationBarTitle((input.score), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
isShowing = true
heptics.impactOccurred()
}) {
Image(systemName: "plus")
}
} // End of ToolbarItem
} // End of Toolbar
} // End of Nav View
} // End of body
} // End of View
struct LocationView: View {
#ObservedObject var input: CityId
#ObservedObject var weatherViewModel: WeatherViewModel
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
input.score = "2643743"
weatherViewModel.stateView = .loading
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "plus")
}
.navigationBarBackButtonHidden(true)
}
}
You may consider adding .onAppear modifier to your first view then you can update the data.
am trying to change the color of the button according to the isSelected state but not working
struct Box: Identifiable {
var id: Int
var title: String
#State var isSelected: Bool
}
struct BoxView: View {
var box: Box
var body: some View{
Button(action: {
self.box.isSelected.toggle()
}){
Text(box.title)
.foregroundColor(.white)
}
.frame(width: 130, height: 50)
.background(self.box.isSelected ? Color.red : Color.blue)
.cornerRadius(25)
.shadow(radius: 10)
.padding(10)
}
}
Try this way.
struct Box: Identifiable {
var id: Int
var title: String
}
struct BoxView: View {
var box: Box
#State var selectedBtn: Int = 1
var body: some View {
ForEach((1...10).reversed(), id: \.self) { item in
Button(action: {
self.selectedBtn = item
}){
Text(self.box.title)
.foregroundColor(.white)
}
.frame(width: 130, height: 50)
.background(self.selectedBtn == item ? Color.red : Color.blue)
.cornerRadius(25)
.shadow(radius: 10)
.padding(10)
}
}
}
you can also observe when value change like this way.
class Box: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var isSelected = false { willSet { objectWillChange.send() } }
}
struct ContentView: View {
#ObservedObject var box = Box()
var body: some View {
VStack{
Button(action: {
self.box.isSelected.toggle()
}){
Text("tap")
.foregroundColor(.white)
}
.background(box.isSelected ?? false ? Color.red : Color.blue)
.cornerRadius(25)
.shadow(radius: 10)
.padding(10)
}
}
}
You can change Button background Color on click using below code
struct ContentView: View {
#State var isSelected : Bool = false
var body: some View {
VStack {
Button(action: {
self.isSelected.toggle()
}){
Text("State")
.foregroundColor(.white)
}
.frame(width: 130, height: 50)
.background(self.isSelected ? Color.red : Color.blue)
}
}
}
I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Let's get you signed in.")
.bold()
.font(.system(size: 40))
.multilineTextAlignment(.leading)
.frame(width: 300, height: 100, alignment: .topLeading)
.padding(Edge.Set.bottom, 50)
Text("Email address:")
.font(.headline)
TextField("Email", text: $email)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Text("Password:")
.font(.headline)
SecureField("Password", text: $password)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Button(action: {
print("login tapped")
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal,30)
}
.navigationBarTitle(Text("Login"))
}
To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.
#State var selection: Int? = nil
Then update your button code as follow to add NavigationLink
NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.
I hope this will help you.
iOS 16+
Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath.
Usage #1
A new view is activated by a simple NavigationLink:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: "NewView") {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #2
A new view is activated by a standard Button:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append("NewView")
} label: {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #3
A new view is activated programmatically:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Content View")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
.onAppear {
path.append("NewView")
}
}
}
iOS 13+
The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.
However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)
Usage #1
NavigationLink is activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
}
.navigationBarTitle(Text("Login"))
}
}
}
Usage #2
NavigationLink is hidden and activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
}
}
Usage #3
NavigationLink is hidden and activated programmatically:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
.onAppear {
self.isLinkActive = true
}
}
}
Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.
Another approach:
SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
BaseView
import SwiftUI
struct BaseView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "view1" {
FirstView()
} else if viewRouter.currentPage == "view2" {
SecondView()
.transition(.scale)
}
}
}
}
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
BaseView().environmentObject(ViewRouter())
}
}
#endif
ViewRouter
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "view1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
FirstView
import SwiftUI
struct FirstView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {self.viewRouter.currentPage = "view2"}) {
NextButtonContent()
}
}
}
}
#if DEBUG
struct FirstView_Previews : PreviewProvider {
static var previews: some View {
FirstView().environmentObject(ViewRouter())
}
}
#endif
struct NextButtonContent : View {
var body: some View {
return Text("Next")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
SecondView
import SwiftUI
struct SecondView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Spacer(minLength: 50.0)
Button(action: {self.viewRouter.currentPage = "view1"}) {
BackButtonContent()
}
}
}
}
#if DEBUG
struct SecondView_Previews : PreviewProvider {
static var previews: some View {
SecondView().environmentObject(ViewRouter())
}
}
#endif
struct BackButtonContent : View {
var body: some View {
return Text("Back")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
Hope this helps!
Simplest and most effective solution is :
NavigationLink(destination:ScoresTableView()) {
Text("Scores")
}.navigationBarHidden(true)
.frame(width: 90, height: 45, alignment: .center)
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.contentShape(Rectangle())
.padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))
ScoresTableView is the destination view.
In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.
struct ButtonNavigationView: View {
#State private var isShowingSecondView : Bool = false
var body: some View {
NavigationStack {
VStack{
Button(action:{isShowingSecondView = true} ){
Text("Show second view")
}
}.navigationDestination(isPresented: $isShowingSecondView) {
Text("SecondView")
}
}
}
}
I think above answers are nice, but simpler way should be:
NavigationLink {
TargetView()
} label: {
Text("Click to go")
}
I'm trying to recreate the iOS 11/12 App Store with SwiftUI.
Let's imagine the "story" is the view displayed when tapping on the card.
I've done the cards, but the problem I'm having now is how to do the animation done to display the "story".
As I'm not good at explaining, here you have a gif:
Gif 1
Gif 2
I've thought of making the whole card a PresentationLink, but the "story" is displayed as a modal, so it doesn't cover the whole screen and doesn't do the animation I want.
The most similar thing would be NavigationLink, but that then obliges me to add a NavigationView, and the card is displayed like another page.
I actually do not care whether its a PresentationLink or NavigationLink or whatever as long as it does the animation and displays the "story".
Thanks in advance.
My code:
Card.swift
struct Card: View {
var icon: UIImage = UIImage(named: "flappy")!
var cardTitle: String = "Welcome to \nCards!"
var cardSubtitle: String = ""
var itemTitle: String = "Flappy Bird"
var itemSubtitle: String = "Flap That!"
var cardCategory: String = ""
var textColor: UIColor = UIColor.white
var background: String = ""
var titleColor: Color = .black
var backgroundColor: Color = .white
var body: some View {
VStack {
if background != "" {
Image(background)
.resizable()
.frame(width: 380, height: 400)
.cornerRadius(20)
} else {
RoundedRectangle(cornerRadius: 20)
.frame(width: 400, height: 400)
.foregroundColor(backgroundColor)
}
VStack {
HStack {
VStack(alignment: .leading) {
if cardCategory != "" {
Text(verbatim: cardCategory.uppercased())
.font(.headline)
.fontWeight(.heavy)
.opacity(0.3)
.foregroundColor(titleColor)
//.opacity(1)
}
HStack {
Text(verbatim: cardTitle)
.font(.largeTitle)
.fontWeight(.heavy)
.lineLimit(3)
.foregroundColor(titleColor)
}
}
Spacer()
}.offset(y: -390)
.padding(.bottom, -390)
HStack {
if cardSubtitle != "" {
Text(verbatim: cardSubtitle)
.font(.system(size: 17))
.foregroundColor(titleColor)
}
Spacer()
}
.offset(y: -50)
.padding(.bottom, -50)
}
.padding(.leading)
}.padding(.leading).padding(.trailing)
}
}
So
Card(cardSubtitle: "Welcome to this library I made :p", cardCategory: "CONNECT", background: "flBackground", titleColor: .white)
displays:
SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.
One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.
In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.
struct ContentView: View {
#State var isPresenting = false
#State var isFullscreen = false
#State var sourceRect: CGRect? = nil
var body: some View {
ZStack {
GeometryReader { proxy in
Button(action: {
self.isFullscreen = false
self.isPresenting = true
self.sourceRect = proxy.frame(in: .global)
}) { ... }
}
if isPresenting {
GeometryReader { proxy in
ModalView()
.frame(
width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil,
height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
.position(
self.isFullscreen ? proxy.frame(in: .global).center :
self.sourceRect?.center ?? proxy.frame(in: .global).center)
.onAppear {
withAnimation {
self.isFullscreen = true
}
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.
Link to Official Documentation
Here's a minimal example:
struct SomeView: View {
#State var isPresented = false
#Namespace var namespace
var body: some View {
VStack {
Button(action: {
withAnimation {
self.isPresented.toggle()
}
}) {
Text("Toggle")
}
SomeSourceContainer {
MatchedView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
}
if isPresented {
SomeTargetContainer {
MatchedTargetView()
.matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
}
}
}
}
}