I'm new to Swift and I'm currently developing my own Timer Application for practice purposes. (I do it without storyboard)
Now I have the Problem that i made a View called "TimePickerView" (Code below), where I created my own Picker. Then I use that TimePickerView in another part of my Application with other Views (in a View). In that View I want to pick my time but I don't know how i can get the Values of the Picker (The Picker works by the way)
This is my TimePickerView
import SwiftUI
struct TimePickerView: View {
#State private var selectedTimeIndexSecond = 0
#State private var selectedTimeIndexMinutes = 0
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(minutes[selectedTimeIndexMinutes]) min : \(seconds[selectedTimeIndexSecond]) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
func getValues() -> (Int, Int) {
return (self.minutes[selectedTimeIndexMinutes] ,self.seconds[selectedTimeIndexSecond])
}
}
and that is the View I want to use my Picker, but I don't know how I get those values from the Picker:
struct SetTimerView: View {
#State var timepicker = TimePickerView()
var body: some View {
NavigationView{
VStack{
//Select the time
timepicker
//Timer variables (This doesn't work)
var timerTime = timepicker.getValues()
var minutes = timerTime.0
var seconds = timerTime.1
Spacer()
let valid : Bool = isValid(timerTime: minutes+seconds)
//Confirm the time
NavigationLink(
destination:
getRightView(
validBool: valid,
timerTime: minutes*60 + seconds),
label: {
ConfirmButtonView(buttonText: "Confirm")
});
Spacer()
}
}
}
func isValid(timerTime : Int) -> Bool {
if (timerTime == 0) {
return false
} else {
return true
}
}
#ViewBuilder func getRightView(validBool : Bool, timerTime : Int) -> some View{
if (validBool == true) {
TimerView(userTime: CGFloat(timerTime), name: "David", isActive: true)
} else {
UnvalidTimeView()
}
}
}
I think main problem is misunderstanding conceptions between data and views.
At first you need a model witch will override your data (create it in separate swift file):
import Foundation
class Time: ObservableObject {
#Published var selectedTimeIndexMinutes: Int = 0
#Published var selectedTimeIndexSecond: Int = 0
}
Pay attention on ObservableObject so that swiftUI can easily detect changes to it that trigger any active views to redraw.
Next I try to change the value of the model in the view
import SwiftUI
struct TimePickerView: View {
#EnvironmentObject var timeData: Time
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $timeData.selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $timeData.selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(timeData.selectedTimeIndexMinutes) min : \(timeData.selectedTimeIndexSecond) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
.environmentObject(Time())
}
}
Like you can see I don't using #Blinding, instead of it I connecting our Model with a View
On the next view I can see changes, I created a new one because your example have view that don't indicated here...
import SwiftUI
struct ReuseDataFromPicker: View {
#EnvironmentObject var timeData: Time
var body: some View {
VStack{
Text("You selected")
Text("\(timeData.selectedTimeIndexMinutes) min and \(timeData.selectedTimeIndexSecond) sec")
}
}
}
struct ReuseDataFromPicker_Previews: PreviewProvider {
static var previews: some View {
ReuseDataFromPicker()
.environmentObject(Time())
}
}
And collect all in a Content View
struct ContentView: View {
var body: some View {
TabView {
TimePickerView()
.tabItem {Label("Set Timer", systemImage: "clock.arrow.2.circlepath")}
ReuseDataFromPicker()
.tabItem {Label("Show Timer", systemImage: "hourglass")}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Time())
}
}
Like that you can easily change or reuse your data on any other views
Related
I created my first simple card game.
https://codewithchris.com/first-swiftui-app-tutorial/
Now I want to add a pop up window which will pop up with the message "You win" if the player gets 15 points and "You lose" if the CPU gets 15 points first.
Can someone please help me how to do it?
I would be glad if there is some tutorial so I can do it myself, not just copy and paste it.
import SwiftUI
struct ContentView: View {
#State private var playCard = "card5"
#State private var cpuCard = "card9"
#State private var playerScore = 0
#State private var cpuScore = 0
var body: some View {
ZStack {
Image("background-plain")
.resizable()
.ignoresSafeArea()
VStack{
Spacer()
Image("logo")
HStack{
Spacer()
Image(playCard)
Spacer()
Image(cpuCard)
Spacer()
}
Button(action: {
//reset
playerScore = 0
cpuScore = 0
}, label: {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: 60))
.foregroundColor(Color(.systemRed)) })
Button(action: {
//gen. random betw. 2 and 14
let playerRand = Int.random(in: 2...14)
let cpuRand = Int.random(in: 2...14)
//Update the cards
playCard = "card" + String(playerRand)
cpuCard = "card" + String(cpuRand)
//Update the score
if playerRand > cpuRand {
playerScore += 1
}
else if cpuRand > playerRand {
cpuScore += 1
}
}, label: {
Image("button")
})
HStack{
Spacer()
VStack{
Text("Player")
.font(.headline)
.padding(.bottom, 10.0)
Text(String(playerScore))
.font(.largeTitle)
}
Spacer()
VStack{
Text("CPU")
.font(.headline)
.padding(.bottom, 10.0)
Text(String(cpuScore))
.font(.largeTitle)
}
Spacer()
}
.foregroundColor(.white)
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Make your popup like a view. And after that. Call it in method present a view.
struct ContentView : View {
#State var showingPopup = false // 1
var body: some View {
ZStack {
Color.red.opacity(0.2)
Button("Push me") {
showingPopup = true // 2
}
}
.popup(isPresented: $showingPopup) { // 3
ZStack { // 4
Color.blue.frame(width: 200, height: 100)
Text("Popup!")
}
}
}
}
extension View {
public func popup<PopupContent: View>(
isPresented: Binding<Bool>,
view: #escaping () -> PopupContent) -> some View {
self.modifier(
Popup(
isPresented: isPresented,
view: view)
)
}
}
public struct Popup<PopupContent>: ViewModifier where PopupContent: View {
init(isPresented: Binding<Bool>,
view: #escaping () -> PopupContent) {
self._isPresented = isPresented
self.view = view
}
/// Controls if the sheet should be presented or not
#Binding var isPresented: Bool
/// The content to present
var view: () -> PopupContent
}
I have managed to set up a Navigationstack that I use programatically to insert/remove views. In my FindOrderView, I want to carry over the variable "ordernumber" to my LoadingScreen(View) using Binding, but I get an error saying my LoadingScreen does not conform to type 'Equatable'. What could I be doing wrong, and is there any other efficient way to get this working?
I'm a complete beginner with SwiftUi but have searched for hours before posting this here. Thankful for any help.
NavigationRouter (Class used to control layers of views)
import Foundation
import SwiftUI
enum Route: Hashable {
case ContentView
case View1
}
final class NavigationRouter: ObservableObject {
#Published var navigationPath = NavigationPath()
func pushView(route: Route) {
navigationPath.append(route)
}
func popToRootView() {
navigationPath = .init()
}
func popToSpecificView(k: Int) {
navigationPath.removeLast(k)
}
}
Mainapp (App starts here but instantly jumps to MainScreenView())
import SwiftUI
#main
struct AvhamtningApp: App {
#StateObject var router = NavigationRouter()
var body: some Scene {
WindowGroup {
NavigationStack(path: $router.navigationPath) {
MainScreenView()
.navigationDestination(for: Route.self) { route in
switch route {
case .ContentView:
MainScreenView()
case .View1:
FindOrderView()
}
}
}.environmentObject(router)
}
}
}
MainScreenView (View with button called "Get order" that carries you over to View1 (FindOrderView))
import SwiftUI
struct MainScreenView: View {
#EnvironmentObject var router: NavigationRouter
var body: some View {
VStack() {
Text("Testing")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.yellow)
.padding(.top, 50)
Spacer()
PrimaryButton(text: "Get Order")
.onTapGesture {
router.pushView(route: .View1)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.white)).ignoresSafeArea(.all)
}
}
struct MainScreenView_Previews: PreviewProvider {
static var previews: some View {
MainScreenView()
}
}
FindOrderView (View with Keypad, if enter 7 digits then should carry you over to Loadingscreen() with the variable "ordernumber" as binding)
import SwiftUI
var list = [["1","2","3"],["4","5","6"],["7","8","9"],["","0","⌫"]]
struct FindOrderView: View {
#State private var ordernumber: String = ""
#EnvironmentObject var router: NavigationRouter
var body: some View {
VStack(spacing: 25) {
Text(ordernumber)
.font(.largeTitle)
.foregroundColor(.black)
.onChange(of: ordernumber) { newValue in
if ordernumber.count >= 7 {
router.navigationPath.append(LoadingScreen(ordernumber: $ordernumber))
}
}
Spacer()
ForEach(list.indices, id: \.self) { listindex in
HStack{
ForEach(list[listindex], id: \.self) { variableindex in
Text(variableindex)
.foregroundColor(.blue)
.font(.system(size: 60))
.onTapGesture(perform: {
if variableindex == "⌫" && !ordernumber.isEmpty {
ordernumber.removeLast()
}
else if ordernumber.count >= 7 {return}
else if variableindex == "⌫" {
()
}
else {
ordernumber += variableindex
}})
.frame(maxWidth: .infinity)
}
}
}
}.background(.white)
}
}
struct FindOrderView_Previews: PreviewProvider {
static var previews: some View {
FindOrderView()
}
}
LoadingScreen (This is where I get the error "Type 'LoadingScreen' does not conform to protocol 'Equatable'")
import SwiftUI
struct LoadingScreen: Hashable, View {
#EnvironmentObject var router: NavigationRouter
#Binding var ordernumber: String
var body: some View {
ZStack{
Text("Loading...")
.font(Font.custom("Baskerville-Bold", size: 50))
.foregroundColor(.orange)
.padding(.bottom, 200)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .orange))
.scaleEffect(2)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.white)).ignoresSafeArea(.all)
}
}
struct LoadingScreen_Previews: PreviewProvider {
static var previews: some View {
LoadingScreen(ordernumber: .constant("constant"))
}
}
So the app starts on the MainScreenView because it's set as the root of the NavigationStack.
Your issue with navigation is that you're trying to send the view as a navigation destination value, but you need to send the route value, and switch on the navigationDestination to generate the view to be pushed.
So in your Route enum, add a route for loading:
case loading(orderNumber: String)
Then in your NavigationLink send that new route:
NavigationLink(value: Route.loadingScreen(orderNumber: orderNumber)
Then in your navigationDestination view modifier:
.navigationDestination(for: Route.self) { route in
switch route {
case .ContentView:
MainScreenView()
case .View1:
FindOrderView()
case .loading(let orderNumber):
LoadingScreen(orderNumber: orderNumber)
.environmentObject(router)
}
}
Hope this helps!
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()
}
}
}
My App calculate for divers some important Values like max. depth etc in meter or in feet. The calculation is written in a separate File. I have a Problem with the segmented Picker toggle. If I launch the App the segmented Picker (Metric <-> Imperial) is on Metric. When I change the segmented Picker on Imperial nothing happened. But when I click on the Button and change the Value (32%, 33%) and push done the results stands in ft. . When I change the segmented Picker Back to Metric nothing happened again. The View is only updated when I change the Value (32%, 33%, …), but I want that the Body refresh when the segmented Picker (Metric <-> Imperial) has a new "position".
The App UI
import SwiftUI
import Combine
struct ContentView: View {
#State var unitSelection = UserDefaults.standard.integer(forKey: "Picker")
#State var O2_2 = 32
#State var PO2_2 = 1.2
var body: some View {
ZStack {
VStack {
Picker("", selection: $unitSelection) {
Text("Metric").tag(0)
Text("Imperial").tag(1)
}
.pickerStyle(SegmentedPickerStyle()).padding(.horizontal, 89)
.onReceive(Just(unitSelection)) {
UserDefaults.standard.set($0, forKey: "Picker")
switchMeasurement(measurement: $0)
}
Spacer()
}
BasicStructure(valueIndexO2Calculate_3: $O2_2, PO2_1: $PO2_2, unitSelection_1: unitSelection)
}
}
}
struct BasicStructure: View {
//Variable from Picker: Calculate -> O2
#Binding var valueIndexO2Calculate_3: Int
#Binding var PO2_1: Double
var unitSelection_1: Int
var body: some View {
VStack {
HStack {
Spacer()
}
Calculate(O2: $valueIndexO2Calculate_3, PO2: $PO2_1, unitSelection_2: unitSelection_1)
ButtonMOD(valueIndexO2Calculate_2: self.$valueIndexO2Calculate_3, unitSelection_0: unitSelection_1)
Spacer()
}
}
}
struct Calculate: View {
#Binding var O2: Int
#Binding var PO2: Double
var unitSelection_2: Int
var body: some View {
VStack {
//O2 max. depth
HStack (alignment: .center){
VStack(alignment: .leading) {
Text("O2 max. depth")
Text("MOD O2")
}
Spacer()
Text(calculateMod02(o2: self.O2, po2: Float(self.PO2)))
.padding(.trailing)
}
.padding(.top).padding(.horizontal)
Divider()
//eq. air depth
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text("eq. air depth")
Text("EAD")
}
Spacer()
Text(calculateEAD(o2: self.O2, po2: Float(self.PO2)))
.padding(.trailing)
}
.padding(.horizontal)
Divider()
//no deco time
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text("no deco time")
Text("DECO 2000")
}
Spacer()
Text("\(calculateDeco2000(o2: self.O2, po2: Float(self.PO2)), specifier: "%.0f")'")
.padding(.trailing)
}
.padding(.horizontal)
Divider()
//max. dive time
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text("max. dive time")
Text("CNS")
}
Spacer()
Text("\(calculateCNS(o2: self.O2, po2: Float(self.PO2)), specifier: "%.0f")'")
.padding(.trailing)
}
.padding(.horizontal).padding(.bottom)
}
.padding()
}
}
struct ButtonMOD: View {
#State var showingDetail_O2 = false
#State var showingDetail_PO2 = false
//Variable from Picker: Calculate -> O2
#Binding var valueIndexO2Calculate_2: Int
var unitSelection_0: Int
var body: some View {
VStack {
HStack(alignment: .center) {
VStack(alignment: .leading) {
Button(action: {
self.showingDetail_O2.toggle()
}) {
Text("\(self.valueIndexO2Calculate_2)%")
Text("O2")
}.sheet(isPresented: $showingDetail_O2) {
SheetViewO2(showSheetView: self.$showingDetail_O2, valueIndexO2Calculate_1: self.$valueIndexO2Calculate_2, unitSelection_3: self.unitSelection_0)
}
}
}
}.padding()
.padding()
}
}
struct SheetViewO2: View {
#Binding var showSheetView: Bool
//Variable from Picker: Calculate -> O2
#Binding var valueIndexO2Calculate_1: Int
var unitSelection_3: Int
var body: some View {
NavigationView {
ValueO2(valueIndexO2Calculate_0: $valueIndexO2Calculate_1, unitSelection_4: unitSelection_3)
.navigationBarTitle(Text("Select value"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
self.showSheetView = false
}) {
Text("Done")
.bold()
})
}.navigationViewStyle(StackNavigationViewStyle())
}
}
//Show "Picker O2"
struct ValueO2: View {
//Variable from Picker: Calculate -> O2
#Binding var valueIndexO2Calculate_0: Int
var unitSelection_4: Int
#State var valueArray : [Int] = []
var body: some View {
VStack{
Section {
Text("O2")
Picker("",selection: $valueIndexO2Calculate_0) {
ForEach(valueArray, id: \.self){ value in
Text("\(value) %")
}
}
}
.labelsHidden()
}.onAppear{
self.initPickerIndex()
}
}
func initPickerIndex(){
valueArray = []
for index1 in 21..<50 {
valueArray.append(index1)
}
for index2 in 1...5{
valueArray.append(40 + index2 * 10)
}
}
}
You don't need binding in this scenario, use direct value for BasicStructure and below (ie. similarly in subviews), like
BasicStructure(valueIndexO2Calculate_3: $O2_2, PO2_1: $PO2_2,
unitSelection_1: unitSelection) // << here !!
and
struct BasicStructure: View {
//Variable from Picker: Calculate -> O2
#Binding var valueIndexO2Calculate_3: Int
#Binding var PO2_1: Double
var unitSelection_1: Int // << here !!
thus changing value in picker makes body refresh and dependent subviews rebuild.
I build a macOS app in swiftui
i try to create a listview where the first item is preselected. i tried it with the 'selected' state of the navigationLink but it didn't work.
Im pretty much clueless and hope you guys can help me.
The code for creating this list view looks like this.
//personList
struct PersonList: View {
var body: some View {
NavigationView
{
List(personData) { person in
NavigationLink(destination: PersonDetail(person: person))
{
PersonRow(person: person)
}
}.frame(minWidth: 300, maxWidth: 300)
}
}
}
(Other views at the bottom)
This is the normal View when i open the app.
When i click on an item its open like this. Thats the state i want as default opening state when i render this view.
The Code for this view looks like this:
//PersonRow
struct PersonRow: View {
//variables definied
var person: Person
var body: some View {
HStack
{
person.image.resizable().frame(width:50, height:50)
.cornerRadius(25)
.padding(5)
VStack (alignment: .leading)
{
Text(person.firstName + " " + person.lastName)
.fontWeight(.bold)
.padding(5)
Text(person.nickname)
.padding(5)
}
Spacer()
}
}
}
//personDetail
struct PersonDetail: View {
var person : Person
var body: some View {
VStack
{
HStack
{
VStack
{
CircleImage(image: person.image)
Text(person.firstName + " " + person.lastName)
.font(.title)
Text("Turtle Rock")
.font(.subheadline)
}
Spacer()
Text("Subtitle")
.font(.subheadline)
}
Spacer()
}
.padding()
}
}
Thanks in advance!
working example. See how selection is initialized
import SwiftUI
struct Detail: View {
let i: Int
var body: some View {
Text("\(self.i)").font(.system(size: 150)).frame(maxWidth: .infinity)
}
}
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack(alignment: .top) {
NavigationView {
List {
ForEach(0 ..< 10) { (i) in
NavigationLink(destination: Detail(i: i), tag: i, selection: self.$selection) {
VStack {
Text("Row \(i)")
Divider()
}
}
}.onAppear {
if self.selection != nil {
self.selection = 0
}
}
}.frame(width: 100)
}
}.background(Color.init(NSColor.controlBackgroundColor))
}
}
screenshot
You can define a binding to the selected row and used a List reading this selection. You then initialise the selection to the first person in your person array.
Note that on macOS you do not use NavigationLink, instead you conditionally show the detail view with an if statement inside your NavigationView.
If person is not Identifiable you should add an id: \.self in the loop. This ressembles to:
struct PersonList: View {
#Binding var selectedPerson: Person?
var body: some View {
List(persons, id: \.self, selection: $selectedPerson) { person in // persons is an array of persons
PersonRow(person: person).tag(person)
}
}
}
Then in your main window:
struct ContentView: View {
// First cell will be highlighted and selected
#State private var selectedPerson: Person? = person[0]
var body: some View {
NavigationView {
PersonList(selectedPerson: $selectedPerson)
if selectedPerson != nil {
PersonDetail(person: person!)
}
}
}
}
Your struct person should be Hashable in order to be tagged in the list. If your type is simple enough, adding Hashable conformance should be sufficient:
struct Person: Hashable {
var name: String
// ...
}
There is a nice tutorial using the same principle here if you want a more complete example.
Thanks to this discussion, as a MacOS Beginner, I managed a very basic NavigationView with a list containing two NavigationLinks to choose between two views. I made it very basic to better understand. It might help other beginners.
At start up it will be the first view that will be displayed.
Just modify in ContentView.swift, self.selection = 0 by self.selection = 1 to start with the second view.
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("(1) Hello, I am the first view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
var body: some View {
Text("(2) Hello, I am the second View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack() {
NavigationView {
List () {
NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
Text("Click Me To Display The First View")
} // End Navigation Link
NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
Text("Click Me To Display The Second View")
} // End Navigation Link
} // End list
.frame(minWidth: 350, maxWidth: 350)
.onAppear {
self.selection = 0
}
} // End NavigationView
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
} // End HStack
} // End some View
} // End ContentView
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Result:
import SwiftUI
struct User: Identifiable {
let id: Int
let name: String
}
struct ContentView: View {
#State private var users: [User] = (1...10).map { User(id: $0, name: "user \($0)")}
#State private var selection: User.ID?
var body: some View {
NavigationView {
List(users) { user in
NavigationLink(tag: user.id, selection: $selection) {
Text("\(user.name)'s DetailView")
} label: {
Text(user.name)
}
}
Text("Select one")
}
.onAppear {
if let selection = users.first?.ID {
self.selection = selection
}
}
}
}
You can use make the default selection using onAppear (see above).