what I am trying to accomplish is have a loop of items where I am able to tap one and it gets bigger programmatically once tapped
here is my code and my results so far:
struct ContentView: View {
#State var emojisArray = ["📚", "🎹", "🎯", "💻"]
#State var selectedIndex = 0
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(0..<emojisArray.count) { item in
emojiView(emoji: self.emojisArray[item],
isSelected: item == self.selectedIndex ? true : false)
.onTapGesture {
print (item)
self.selectedIndex = item
}
}
}
}
.onAppear()
.frame(height:160)
VStack{
Text("selcted item:")
Text("\(self.emojisArray[self.selectedIndex])")
}
}
}
}
where emojiView is :
struct emojiView: View {
var emoji : String
#State var isSelected : Bool
var body: some View {
Text(emoji)
.font(isSelected ? .system(size: 120) : .system(size: 45))
}
}
I guess the problem is that the ScrollView does't reload itself
Just remove #State in emojiView
struct emojiView: View {
var emoji : String
var isSelected : Bool // << here !!
var body: some View {
Text(emoji)
.font(isSelected ? .system(size: 120) : .system(size: 45))
}
}
Tested with Xcode 12 / iOS 14
Related
I've built a view that has scroll view of horizontal type with HStack for macOS app. Is there a way to circle those items using keyboard arrows?
(I see that ListView has a default behavior but for other custom view types there are none)
click here to see the screenshot
var body: some View {
VStack {
ScrollView(.horizontal, {
HStack {
ForEach(items.indices, id: \.self) { index in
//custom view for default state and highlighted state
}
}
}
}
}
any help is appreciated :)
You could try this example code, using my previous post approach, but with a horizontal scrollview instead of a list. You will have to adjust the code to your particular app. My approach consists only of a few lines of code that monitors the key events.
import Foundation
import SwiftUI
import AppKit
struct ContentView: View {
let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
#State var selection: Int = 0
#State var keyMonitor: Any?
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
ForEach(fruits.indices, id: \.self) { index in
VStack {
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(10)
Text(fruits[index]).tag(index)
}
.background(selection == index ? Color.red : Color.clear)
.padding(10)
}
}
}
.onAppear {
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
if nsevent.keyCode == 124 { // arrow right
selection = selection < fruits.count ? selection + 1 : 0
} else {
if nsevent.keyCode == 123 { // arrow left
selection = selection > 1 ? selection - 1 : 0
}
}
return nsevent
}
}
.onDisappear {
if keyMonitor != nil {
NSEvent.removeMonitor(keyMonitor!)
keyMonitor = nil
}
}
}
}
Approach I used
Uses keyboard shortcuts on a button
Alternate approach
To use commands (How to detect keyboard events in SwiftUI on macOS?)
Code:
Model
struct Item: Identifiable {
var id: Int
var name: String
}
class Model: ObservableObject {
#Published var items = (0..<100).map { Item(id: $0, name: "Item \($0)")}
}
Content
struct ContentView: View {
#StateObject private var model = Model()
#State private var selectedItemID: Int?
var body: some View {
VStack {
Button("move right") {
moveRight()
}
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
ScrollView(.horizontal) {
LazyHGrid(rows: [GridItem(.fixed(180))]) {
ForEach(model.items) { item in
ItemCell(
item: item,
isSelected: item.id == selectedItemID
)
.onTapGesture {
selectedItemID = item.id
}
}
}
}
}
}
private func moveRight() {
if let selectedItemID {
if selectedItemID + 1 >= model.items.count {
self.selectedItemID = model.items.last?.id
} else {
self.selectedItemID = selectedItemID + 1
}
} else {
selectedItemID = model.items.first?.id
}
}
}
Cell
struct ItemCell: View {
let item: Item
let isSelected: Bool
var body: some View {
ZStack {
Rectangle()
.foregroundColor(isSelected ? .yellow : .blue)
Text(item.name)
}
}
}
I have a view made up of 3 files. This view is a marketplace. There are two options to pick from in the filter tab bar at the top of the view. The view changes in response to these tabs successfully. When an item on the marketplace is tapped, a detailed view of the item is displayed.
However, when the detail view is displayed, the filter tab bar remains at the top of the view. I need this filter tab bar to hide when a detail view variable is set to true.
This is my code:
Marketplace code:
import SwiftUI
struct Marketplace: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#State private var selectedMarketplaceFilter: MarketplaceFilterViewModel = .shoe
// MARK: For Smooth Sliding Effect
#Namespace var animation
// Shared Data...
#EnvironmentObject var sharedData: SharedDataModel
#State var shoeData : Shoe = Shoe()
#State var showDetailShoe: Bool = false
#State var sockData : Sock = Sock()
#State var showDetailSock: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
if !showDetailShoe && !showDetailSock {
marketplaceFilterBar
}
if selectedMarketplaceFilter == .shoes {
MarketplaceShoeView()
}
if selectedMarketplaceFilter == .socks {
MarketplaceSockView()
}
}
}
}
//Filter bar view
var marketplaceFilterBar: some View {
VStack {
HStack {
ForEach(MarketplaceFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.headline)
.fontWeight(selectedMarketplaceFilter == item ? .semibold: .regular)
.foregroundColor(selectedMarketplaceFilter == item ? .black: .gray)
if selectedMarketplaceFilter == item {
Capsule()
.foregroundColor(Color("Blue"))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
} else {
Capsule()
.foregroundColor(Color(.clear))
.frame(height: 3)
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectedMarketplaceFilter = item
}
}
}
}
}
}
}
MarketplaceShoeView(same exact code as MarketplaceSockView):
import SwiftUI
struct MarketplaceShoeView: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#State private var selectedMarketplaceFilter: MarketplaceFilterViewModel = .shoe
#Namespace var animation
#State var showDetailShoe = false
#State var selectedShoe : Shoe!
// Shared Data...
#EnvironmentObject var sharedData: SharedDataModel
#State var string = ""
var body: some View {
var columns = Array(repeating: GridItem(.flexible()), count: 2)
ZStack{
VStack(spacing: 10){
HStack(spacing: 5) {
HStack(spacing: 12){
Image(systemName: "magnifyingglass")
TextField("Search", text: $MarketplaceModel.search)
}
}
if MarketplaceModel.shoes.isEmpty{
ProgressView()
}
else{
ScrollView(.vertical, showsIndicators: false, content: {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(),spacing: 10), count: 2),spacing: 20){
ForEach(MarketplaceModel.filteredShoe){shoe in
ShoeView(shoeData: shoe)
.onTapGesture {
withAnimation(.easeInOut){
selectedShoe = shoe
showDetailShoe.toggle()
}
}
}
}
})
}
}
if selectedShoe != nil && showDetailShoe{
ShoeDetailView(showDetailShoe: $showDetailShoe, shoeData: selectedShoe, shoe_id: string)
}
}
}
}
ShoeDetailView:
struct ShoeDetailView: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#Binding var showDetailShoe: Bool
var shoeData : Shoe
var shoe_id: String
#Namespace var animation: Namespace.ID
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
#State var string = ""
var body: some View {
NavigationView{
VStack{
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailShoe.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
ForEach(MarketplaceModel.shoe_details_array){ items in
Text(items.shoe_name)
}
}
}
Image("ShoeImage")
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 15) {
Text(shoeData.shoe_name)
Text(shoeData.shoe_details)
}
}
}
}
}
}
SharedDataModel:
class SharedDataModel: ObservableObject {
#Published var detailSock : Sock?
#Published var showDetailSock : Bool = false
#Published var detailShoe : Shoe?
#Published var showDetailShoe : Bool = false
//Add something here??
}
marketplaceFilterBar needs to be hidden when a detailView is true.
You need to use the boolean values from EnvironmentObject instead of redefining them inside views. As follow:
import SwiftUI
struct Marketplace: View {
...
#EnvironmentObject var sharedData: SharedDataModel
...
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
// Use from env object
if !sharedData.showDetailShoe && !sharedData.showDetailSock {
marketplaceFilterBar
}
...
}
}
}
...
}
and so on...
Update: I refactored it a bit to use #ObservableObject, but the issue still persists
In my SwiftUI app, I have a LazyVGrid populated with n cards and one New Event button. When I press the New Event button, I have a custom modal overlaying the View to let the user add a new event. However, after dismissing the view (by tapping a Create Event button), the modal disappears however the new event card isn't added to the LazyVGrid. It only appears once I force quit the app and re-open it.
Here is the main ContentView:
struct ContentView: View {
#State var progress: Double = 0.0
#State var editMode: Bool = false
#State var angle: Double = 0
#State var showModal: Bool = false
#ObservedObject var model: Model
var columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
func animate(while condition: Bool) {
self.angle = condition ? -0.6 : 0
withAnimation(Animation.linear(duration: 0.125).repeatForever(while: condition)) {
self.angle = 0.6
}
if !condition {
self.angle = 0
}
}
var body: some View {
ZStack {
NavigationView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(model.events, id: \.self) { event in
SmallCardView(event: event, angle: $angle)
}
if !showModal {
AddEventButtonView(
editMode: $editMode,
showModal: $showModal,
namespace: namespace
)
} else {
Spacer().frame(height: 100)
}
}
.onLongPressGesture {
if !self.editMode {
self.editMode = true
animate(while: self.editMode)
}
}
}
if self.showModal {
AddEventView(namespace: namespace, events: self.$model.events, showModal: $showModal)
}
}
}
}
This is the View for the Add Event button:
struct AddEventButtonView: View {
#Binding var editMode: Bool
#Binding var showModal: Bool
var namespace: Namespace.ID
var body: some View {
Image(systemName: "calendar.badge.plus")
.frame(height: 100)
.frame(maxWidth: .infinity)
.opacity(self.editMode ? 0.0 : 1.0)
.onTapGesture {
withAnimation {
self.showModal = true
}
}
}
}
and this is my modal view:
struct AddEventView: View {
#State var eventName: String = ""
#State var endDate = Date()
#State var gradientIndex: Int = 0
#Binding var showModal: Bool
#Binding var events: [Event]
var namespace: Namespace.ID
init(namespace: Namespace.ID, events: Binding<[Event]>, showModal: Binding<Bool>) {
self.namespace = namespace
self._events = events
self._showModal = showModal
}
var body: some View {
VStack(spacing: 30.0) {
//...
Button(action: {
let event = Event(name: eventName, start: .init(), end: endDate, gradientIndex: gradientIndex)
self.events.append(event)
withAnimation {
self.showModal = false
}
}) {
RoundedRectangle(cornerRadius: 13)
.frame(maxWidth: .infinity)
.frame(height: 42)
.overlay(
Text("Add Event")
.foregroundColor(.white)
.bold()
)
}
.disabled(self.eventName.isEmpty)
}
}
}
If anyone has any idea of how to update the LazyVGrid to show the new event card when a new event is added that'd be awesome, thanks!
Here's the problem:
It is not clear how you did set up model, but it should be observed to update view, as (scratchy)
struct ContentView: View {
#State var progress: Double = 0.0
#State var editMode: Bool = false
#State var angle: Double = 0
#State var showModal: Bool = false
#ObservedObject var model: Model // << here !!
...
or use #EnvironmentObject pattern with injection via .environmentObject
or for Swift 2.0 use
#StateObject var model: Model = Model.shared
I have a navigation requirement that looks something like this:
Each detail screen can navigation to the next and previous detail screen. At the same time, the "back" button should always go back to the main list (not the previous detail screen).
I'm struggling with how to accomplish this in SwiftUI?
Here is what I have so far:
struct ListView: View {
#State private var currentDetailShown: Int?
#State private var listItems: [Int] = Array(repeating: 0, count: 10)
func goToNext() {
if let idx = self.currentDetailShown {
self.currentDetailShown = min(self.listItems.count - 1, idx + 1)
}
}
func goToPrev() {
if let idx = self.currentDetailShown {
self.currentDetailShown = max(0, idx - 1)
}
}
var body: some View {
List {
ForEach(0..<listItems.count) { index in
NavigationLink(destination: DetailView(goToNext: self.goToNext, goToPrev: self.goToPrev),
tag: index,
selection: self.$currentDetailShown) {
ListItem(score: listItems[index])
}
.isDetailLink(false)
.onTapGesture {
self.currentDetailShown = index
}
}
}
}
}
What happens with this code is that from the first detail view, it'll move to the to the next detail view and then immediately jump back to the list view.
I feel like I'm overthinking this or missing something obvious...
Instead of navigating to each detail from your list, you can navigate to a detailView that can show each detail individually by using a published variable in an observable object. Here is an example
struct MainView: View{
#EnvironmentObject var viewModel: ViewModel
var body: some View{
NavigationView{
VStack{
ForEach(self.viewModel.details, id:\.self){ detail in
NavigationLink(destination: DetailView(detail: self.viewModel.details.firstIndex(of: detail)!).environmentObject(ViewModel())){
Text(detail)
}
}
}
}
}
}
class ViewModel: ObservableObject{
#Published var showingView = 0
#Published var details = ["detail1", "detail2", "detail3", "detail4", "detail5", "detail6"]
}
struct DetailView: View{
#EnvironmentObject var viewModel: ViewModel
#State var detail: Int
var body: some View{
VStack{
IndivisualDetailView(title: viewModel.details[detail])
Button(action: {
self.viewModel.showingView -= 1
}, label: {
Image(systemName: "chevron.left")
})
Button(action: {
self.viewModel.showingView += 1
print(self.viewModel.showingView)
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct IndivisualDetailView: View{
#State var title: String
var body: some View{
Text(title)
}
}
struct ContentView: View {
#State private var selectedNumber = 0
// var numbersArray - This will be the array
var body: some View {
VStack {
Picker("Number Picker", selection: $selectedNumber) {
ForEach(0..<Int(numbersArray.count)) {
Text("\($0 + 1)").font(.system(size: 60))
}
}
}
}
}
I am creating a Picker for selecting numbers in Watchkit. When I try to increase the font size the numbers are overlapping. How to make the Picker content resize automatically so that the contents does not overlap.
You could add something like Spacer():
struct TestSwiftUIView: View {
#State private var selectedNumber = 0
var numbersArray = [1,2,3,4,5,6]
var body: some View {
VStack {
Picker("Number Picker", selection: $selectedNumber) {
ForEach(0..<Int(numbersArray.count)) {
Spacer()
Text("\($0 + 1)").font(.system(size: 60))
}
}
}
}
}
It's just my first Idea, probably not the best solution.