I want the scrollView to be exact the size of its content. But if I don't set a fixed size (".frame(height: 300)"), it looks like this: Instead it should have the height of its content.
This is my code:
var body: some View {
ScrollView{
VStack(){
Form {
Section(header: Text("User-Settings".localized())) {
if (showAllOptions) {
Toggle(isOn: $includeProjects,
label:
{Text("Include Projects".localized())}
)
}
Toggle(isOn: $muteWatch,
label: {
Text("Mute Watch".localized())
})
if (showAllOptions) {
TextField("Developer Key".localized(), text: $developerKey)
}
Button("Logout".localized()){
showingAlert = true
}
.alert("Delete credentials?".localized(), isPresented: $showingAlert) {
Button("OK".localized()) {
deviceId = ""
userId = ""
employeeId = ""
//userId = ""
//employeeId = ""
}
Button("CANCEL".localized(), role: .destructive) {
}
}
.foregroundColor(.red)
}
if (developerKey == "4711") {
Section(header: Text("Developer-Settings".localized())) {
TextField("Dev URL", text: $developerModeParent)
Button("Reset URL"){
developerModeParent = ""
}
}
}
}//.frame(height: 300)
}.onAppear{
if (userId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) {
showAllOptions = false
} else {
showAllOptions = true
}
showAlertIsRunning = false
includingProjectsCurrent = includeProjects
if (developerModeParent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) {
baseUrl = "http://192.168.179.185:8160" //TODO: später hier gegen produktive URL tauschen
} else {
baseUrl = developerModeParent
}
}
.onDisappear {
if (includeProjects != includingProjectsCurrent) {
Task {
await isWorkingSince()
}
}
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Is there a way I can set the height of the scrollview to its content or just change its size with a parameter I set for a specific event, so its height gets changed if ... happens.
Related
i am building a historical navigation (backwards, forwards) for a macOS app
It works mostly fine, but my implementation relies on the path, which however doesn't contain the root (when the sidebar is selected, and [] is assigned to path)
how to get the root, or to save the root from navigationDestination
import SwiftUI
enum NavigationDestination: Hashable {
case test1
case test2
case test3(text:String)
}
struct SidebarView: View {
var body: some View {
VStack {
NavigationLink(value: NavigationDestination.test1) {
Text("1")
}
NavigationLink(value: NavigationDestination.test2) {
Text("2")
}
NavigationLink(value: NavigationDestination.test3(text:"X")) {
Text("3")
}
}
}
}
struct TestView: View {
let text: String
var body: some View {
VStack {
Text(text)
NavigationLink(value: NavigationDestination.test1) {
Text("1")
}
NavigationLink(value: NavigationDestination.test2) {
Text("2")
}
NavigationLink(value: NavigationDestination.test3(text:"3-1")) {
Text("3-1")
}
NavigationLink(value: NavigationDestination.test3(text:"3-2")) {
Text("3-2")
}
NavigationLink(value: NavigationDestination.test3(text:"3-3")) {
Text("3-3")
}
NavigationLink(value: NavigationDestination.test3(text:"3-4")) {
Text("3-4")
}
NavigationLink(value: NavigationDestination.test3(text:"3-5")) {
Text("3-5")
}
List {
ForEach(0..<200) { index in
Text("LIST \(index)")
}
}
}
}
}
struct MainView: View {
#State private var history: [[NavigationDestination]] = []
#State private var historyIndex: Int = -1
#State private var canGoBackInHistory = false
#State private var canGoForwardInHistory = false
#State private var path: [NavigationDestination] = []
var body: some View {
NavigationSplitView(columnVisibility: .constant(.all)) {
SidebarView()
.navigationDestination(for: NavigationDestination.self) { destination in
detailBuilder(destination)
.navigationBarBackButtonHidden(true)
}
} detail: {
NavigationStack(path: $path) {
Group {
}
.navigationDestination(for: NavigationDestination.self) { destination in
detailBuilder(destination)
.navigationBarBackButtonHidden(true)
}
}
}
.onChange(of: path) { newValue in
if history[safe: historyIndex] == newValue {
return
}
if historyIndex != history.count - 1 {
history.removeLast(history.count - historyIndex)
}
if newValue != [] {
history.append(newValue)
}
historyIndex = history.count - 1
canGoBackInHistory = history.count > 0
canGoForwardInHistory = false
print("======\n \(history)")
}
.toolbar {
ToolbarItemGroup(placement: .navigation) {
Button(action: {
goBackInHistory()
}, label: {
Image(systemName: "chevron.left")
})
.disabled(!canGoBackInHistory)
Button(action: {
goForwardInHistory()
}, label: {
Image(systemName: "chevron.right")
})
.disabled(!canGoForwardInHistory)
}
}
}
func goBackInHistory() {
guard let h = history[safe: historyIndex - 1] else {
return
}
historyIndex -= 1
canGoBackInHistory = historyIndex > 0
canGoForwardInHistory = true
withAnimation {
path = h
}
}
func goForwardInHistory() {
guard let h = history[safe: historyIndex + 1] else {
return
}
historyIndex += 1
canGoBackInHistory = true
canGoForwardInHistory = historyIndex < (history.count - 1)
withAnimation {
path = h
}
}
#ViewBuilder
func detailBuilder(_ destination: NavigationDestination) -> some View {
switch destination {
case .test1:
TestView(text: "1")
case .test2:
TestView(text: "2")
case .test3(let text):
TestView(text: text)
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
extension Collection where Indices.Iterator.Element == Index {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Iterator.Element? {
return indices.contains(index) ? self[index] : nil
}
}
I try to build a counter.
Here is my model:
var encoder = JSONEncoder()
var decoder = JSONDecoder()
class CounterItems: ObservableObject {
#Published var counterItems: [Counter]
var index = 0
init() {
counterItems = []
}
init(data: [Counter]) {
counterItems = []
for item in data {
counterItems.append(Counter(id: index, name: item.name, count: item.count, step: item.step, isDeleted: item.isDeleted))
index += 1
}
}
func AddNewCounter(newCounter: Counter) {
counterItems.append(Counter(id: index, name: newCounter.name, count: newCounter.count, step: newCounter.step, isDeleted: newCounter.isDeleted))
index += 1
storeData()
}
func minus(index: Int) {
if counterItems[index].count >= counterItems[index].step {
counterItems[index].count -= counterItems[index].step
}
storeData()
}
func plus(index: Int) {
counterItems[index].count += counterItems[index].step
storeData()
}
func edit(index: Int, data: Counter) {
counterItems[index].name = data.name
counterItems[index].step = data.step
storeData()
}
func reset(index: Int) {
counterItems[index].count = 0
storeData()
}
func resetAll() {
for item in counterItems {
reset(index: item.id)
}
storeData()
}
func delete(index: Int) {
counterItems[index].isDeleted = true
storeData()
}
func deleteAll() {
for item in counterItems {
delete(index: item.id)
}
storeData()
}
func storeData() {
let dataToStore = try! encoder.encode(counterItems)
UserDefaults.standard.set(dataToStore, forKey: "counterItems")
}
}
Here is my view:
struct ContentView: View {
#ObservedObject var userData: CounterItems = CounterItems(data: initUserData())
#State var isShowingAddCounterView = false
#State var isShowingResetingDialog = false
#State var isShowingDeletingDialog = false
var body: some View {
NavigationView {
VStack {
ScrollView {
VStack {
ForEach(userData.counterItems) { item in
if item.isDeleted == false {
SingleCounterView(index: item.id)
.environmentObject(userData)
}
}
}
}
.navigationTitle("Tally Counter")
HStack(spacing: 130) {
Button(action: {
isShowingResetingDialog = true
}, label: {
Image(systemName: "gobackward")
.imageScale(.large)
.foregroundColor(.accentColor)
})
.alert(isPresented: $isShowingResetingDialog) {
Alert(title: Text("Will reset all counters"),
primaryButton: .default(
Text("Confirm"),
action: {userData.resetAll()}
),
secondaryButton: .cancel(Text("Cancel"))
)
}
Button(action: {
isShowingAddCounterView = true
}, label: {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
.foregroundColor(.accentColor)
})
.sheet(isPresented: $isShowingAddCounterView, content: {
AddCounterView().environmentObject(userData)
})
Button(action: {
isShowingDeletingDialog = true
}, label: {
Image(systemName: "trash")
.imageScale(.large)
.foregroundColor(.accentColor)
})
.alert(isPresented: $isShowingDeletingDialog) {
Alert(title: Text("Will delete all counters!"),
primaryButton: .default(
Text("Confirm"),
action: {userData.deleteAll()}
),
secondaryButton: .cancel(Text("Cancel"))
)
}
}
}
}
}
}
struct SingleCounterView: View {
#EnvironmentObject var userData: CounterItems
var index: Int
#State var isShowingEditingView = false
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color("Color\(index%5 + 1)"))
.frame(height: 150)
.cornerRadius(20)
.padding([.trailing, .leading])
.shadow(radius: 5, x: 0, y: 5)
HStack(spacing: 20) {
Button(action: {
userData.minus(index: index)
HapticManager.instance.impact(style: .medium)
}, label: {
Image(systemName: "minus.circle")
.resizable()
.frame(width: 40, height: 40)
.foregroundColor(.white)
.padding()
})
VStack(spacing: 10) {
Button(action: {
isShowingEditingView = true
}, label: {
VStack(spacing: 10) {
Text(userData.counterItems[index].name)
Text("\(userData.counterItems.first(where: {$0.id == index})!.count)")
.font(.system(size: 60))
.frame(width: 100)
}
})
.sheet(isPresented: $isShowingEditingView, content: {
AddCounterView(userData: _userData, name: userData.counterItems[index].name, step: userData.counterItems[index].step, index: index)
})
NavigationLink(destination: {
SingleCounterFullView(index: index).environmentObject(userData)
}, label: {
Image("quanping")
.resizable()
.frame(width: 20, height: 20)
})
}
.foregroundColor(.white)
.padding()
Button(action: {
userData.plus(index: index)
HapticManager.instance.impact(style: .medium)
}, label: {
Image(systemName: "plus.circle")
.resizable()
.frame(width: 40, height: 40)
.foregroundColor(.white)
.padding()
})
}
}
}
}
func initUserData() -> [Counter] {
var output: [Counter] = []
if let storedData = UserDefaults.standard.object(forKey: "counterItems") as? Data {
let data = try! decoder.decode([Counter].self, from: storedData)
for item in data {
if !item.isDeleted {
output.append(Counter(id: output.count, name: item.name, count: item.count, step: item.step, isDeleted: item.isDeleted))
}
}
}
return output
}
All these functions work well in live preview and simulator.
But when running on my iPhone 12, the count of a single counter in SingleCounterView sometimes is not updated until I press the plus or minus button of another SingleCounterView. However, userData.counterItems[index].count is still updated and works well.
e.g.
I have a counter named as "Books", the current count is 3. When the plus button is pressed, count in SingleCounterView is not updated to 4 until I press another plus or minus button in another counter. But count in SingleCounterFullView is always right and updated after plus or minus is pressed.
Note : The problem can happen on any counter but only on one at a time. Once a counter as the issue, it continue with it. It does not always happens.
Seems that using binding may suppress the problem :
ForEach(userData.counterItems) { item in
let index = item.id
if item.isDeleted == false {
// pass the array item instead of index in array
SingleCounterView(counter: $userData.counterItems[index])
.environmentObject(userData)
}
}
Updated the view to use a binding to the array element
struct SingleCounterBindingView: View {
#EnvironmentObject var userData: CounterItems
#Binding var counter: Counter
var index: Int {
counter.id
}
#State var isShowingEditingView = false
var body: some View {
HStack(spacing: 20) {
Button(action: {
counter.minus()
}, label: {
Image(systemName: "minus.circle")
.resizable()
.frame(width: 40, height: 40)
.padding()
})
VStack(spacing: 10) {
Button(action: {
isShowingEditingView = true
}, label: {
VStack(spacing: 10) {
Text(counter.name)
Text("\(counter.count)")
.font(.system(size: 60))
.frame(width: 100)
}
})
.sheet(isPresented: $isShowingEditingView, content: {
AddCounterView(userData: _userData, name: counter.name, step: counter.step, index: index)
})
NavigationLink(destination: {
SingleCounterFullView(index: index).environmentObject(userData)
}, label: {
Image(systemName: "info")
.resizable()
.frame(width: 20, height: 20)
})
}
.padding()
Button(action: {
counter.plus()
}, label: {
Image(systemName: "plus.circle")
.resizable()
.frame(width: 40, height: 40)
.padding()
})
}
}
}
Created a counter struct to handle modifications :
struct Counter: Codable, Identifiable {
var id: Int
var name: String
var count: Int
var step: Int
var isDeleted: Bool
mutating func plus() {
count += step
}
mutating func minus() {
if count >= step {
count -= step
}
}
mutating func reset() {
count = 0
}
mutating func delete() {
isDeleted = true
}
mutating func edit(from counter: Counter) {
name = counter.name
step = counter.step
}
}
// Updated model to use Counter new functions
class CounterItems: ObservableObject {
#Published var counterItems: [Counter]
var index = 0
init() {
counterItems = []
}
init(data: [Counter]) {
counterItems = []
for item in data {
counterItems.append(Counter(id: index, name: item.name, count: item.count, step: item.step, isDeleted: item.isDeleted))
index += 1
}
}
func addNewCounter(newCounter: Counter) {
counterItems.append(Counter(id: index, name: newCounter.name, count: newCounter.count, step: newCounter.step, isDeleted: newCounter.isDeleted))
index += 1
storeData()
}
func minus(index: Int) {
counterItems[index].minus()
storeData()
}
func plus(index: Int) {
counterItems[index].plus()
storeData()
}
func edit(index: Int, data: Counter) {
counterItems[index].edit(from: data)
storeData()
}
func reset(index: Int) {
counterItems[index].reset()
storeData()
}
func resetAll() {
for item in counterItems {
reset(index: item.id)
}
// note storeData is not needed
}
func delete(index: Int) {
counterItems[index].delete()
storeData()
}
func deleteAll() {
for item in counterItems {
delete(index: item.id)
}
// note storeData is not needed
}
func storeData() {
let dataToStore = try! encoder.encode(counterItems)
UserDefaults.standard.set(dataToStore, forKey: "counterItems")
}
}
Note: I suppresses all colors and haptic to simpilfy sample code
Note 2 : I assume that index and item.id where equivalent as it seems in your example. But index could be a computed var using firstIndex.
Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}
I have text but it's not fit. I want use marquee when text not fit in my default frame.
Text(self.viewModel.soundTrack.title)
.font(.custom("Avenir Next Regular", size: 24))
.multilineTextAlignment(.trailing)
.lineLimit(1)
.foregroundColor(.white)
.fixedSize(horizontal: false, vertical: true)
//.frame(width: 200.0, height: 30.0)
Try below code....
In MarqueeText.swift
import SwiftUI
struct MarqueeText: View {
#State private var leftMost = false
#State private var w: CGFloat = 0
#State private var previousText: String = ""
#State private var contentViewWidth: CGFloat = 0
#State private var animationDuration: Double = 5
#Binding var text : String
var body: some View {
let baseAnimation = Animation.linear(duration: self.animationDuration)//Animation duration
let repeated = baseAnimation.repeatForever(autoreverses: false)
return VStack(alignment:.center, spacing: 0) {
GeometryReader { geometry in//geometry.size.width will provide container/superView width
Text(self.text).font(.system(size: 24)).lineLimit(1).foregroundColor(.clear).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, perform: {
self.w = $0
print("textWidth:\(self.w)")
print("geometry:\(geometry.size.width)")
self.contentViewWidth = geometry.size.width
if self.text.count != self.previousText.count && self.contentViewWidth < self.w {
let duration = self.w/50
print("duration:\(duration)")
self.animationDuration = Double(duration)
self.leftMost = true
} else {
self.animationDuration = 0.0
}
self.previousText = self.text
}).fixedSize(horizontal: false, vertical: true)// This Text is temp, will not be displayed in UI. Used to identify the width of the text.
if self.animationDuration > 0.0 {
Text(self.text).font(.system(size: 24)).lineLimit(nil).foregroundColor(.green).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).onPreferenceChange(WidthPreferenceKey.self, perform: { _ in
if self.text.count != self.previousText.count && self.contentViewWidth < self.w {
} else {
self.leftMost = false
}
self.previousText = self.text
}).modifier(self.makeSlidingEffect().ignoredByLayout()).animation(repeated, value: self.leftMost).clipped(antialiased: true).offset(y: -8)//Text with animation
}
else {
Text(self.text).font(.system(size: 24)).lineLimit(1).foregroundColor(.blue).fixedSize(horizontal: true, vertical: false).background(TextGeometry()).fixedSize(horizontal: false, vertical: true).frame(maxWidth: .infinity, alignment: .center).offset(y: -8)//Text without animation
}
}
}.fixedSize(horizontal: false, vertical: true).layoutPriority(1).frame(maxHeight: 50, alignment: .center).clipped()
}
func makeSlidingEffect() -> some GeometryEffect {
return SlidingEffect(
xPosition: self.leftMost ? -self.w : self.w,
yPosition: 0).ignoredByLayout()
}
}
struct MarqueeText_Previews: PreviewProvider {
#State static var myCoolText = "myCoolText"
static var previews: some View {
MarqueeText(text: $myCoolText)
}
}
struct SlidingEffect: GeometryEffect {
var xPosition: CGFloat = 0
var yPosition: CGFloat = 0
var animatableData: CGFloat {
get { return xPosition }
set { xPosition = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let pt = CGPoint(
x: xPosition,
y: yPosition)
return ProjectionTransform(CGAffineTransform(translationX: pt.x, y: pt.y)).inverted()
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct MagicStuff: ViewModifier {
func body(content: Content) -> some View {
Group {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}
}
}
}
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
In your existing SwiftUI struct.
(The below sample code will check 3 cases 1.Empty string, 2.Short string that doesn't need to marquee, 3.Lengthy marquee string)
#State var value = ""
#State var counter = 0
var body: some View {
VStack {
Spacer(minLength: 0)
Text("Monday").background(Color.yellow)
HStack {
Spacer()
VStack {
Text("One").background(Color.blue)
}
VStack {
MarqueeText(text: $value).background(Color.red).padding(.horizontal, 8).clipped()
}
VStack {
Text("Two").background(Color.green)
}
Spacer()
}
Text("Tuesday").background(Color.gray)
Spacer(minLength: 0)
Button(action: {
self.counter = self.counter + 1
if (self.counter % 2 == 0) {
self.value = "1Hello World! Hello World! Hello World! Hello World! Hello World!"
} else {
self.value = "1Hello World! Hello"
}
}) {
Text("Button")
}
Spacer()
}
}
Install https://github.com/SwiftUIKit/Marquee 0.2.0 above
with Swift Package Manager and try below code....
struct ContentView: View {
var body: some View {
Marquee {
Text("Hello World!")
.font(.system(size: 40))
}
// This is the key point.
.marqueeWhenNotFit(true)
}
}
When you keep increasing the length of the text until it exceeds the width of the marquee, the marquee animation will automatically start.
I was looking for the same thing, but every solution I tried either did not meet my specifications or caused layout/rendering issues, especially when the text changed or the parent view was refreshed. I ended up just writing something from scratch. It is quite hack-y, but it seems to be working now. I would welcome any suggestions on how it can be improved!
import SwiftUI
struct Marquee: View {
#ObservedObject var controller:MarqueeController
var body: some View {
VStack {
if controller.changing {
Text("")
.font(Font(controller.font))
} else {
if !controller.shouldAnimate {
Text(controller.text)
.font(Font(controller.font))
} else {
AnimatedText(controller: controller)
}
}
}
.onAppear() {
self.controller.checkForAnimation()
}
.onReceive(controller.$text) {_ in
self.controller.checkForAnimation()
}
}
}
struct AnimatedText: View {
#ObservedObject var controller:MarqueeController
var body: some View {
Text(controller.text)
.font(Font(controller.font))
.lineLimit(1)
.fixedSize()
.offset(x: controller.animate ? controller.initialOffset - controller.offset : controller.initialOffset)
.frame(width:controller.maxWidth)
.mask(Rectangle())
}
}
class MarqueeController:ObservableObject {
#Published var text:String
#Published var animate = false
#Published var changing = true
#Published var offset:CGFloat = 0
#Published var initialOffset:CGFloat = 0
var shouldAnimate:Bool {text.widthOfString(usingFont: font) > maxWidth}
let font:UIFont
var maxWidth:CGFloat
var textDoubled = false
let delay:Double
let duration:Double
init(text:String, maxWidth:CGFloat, font:UIFont = UIFont.systemFont(ofSize: 12), delay:Double = 1, duration:Double = 3) {
self.text = text
self.maxWidth = maxWidth
self.font = font
self.delay = delay
self.duration = duration
}
func checkForAnimation() {
if shouldAnimate {
let spacer = " "
if !textDoubled {
self.text += (spacer + self.text)
self.textDoubled = true
}
let textWidth = self.text.widthOfString(usingFont: font)
self.initialOffset = (textWidth - maxWidth) / 2
self.offset = (textWidth + spacer.widthOfString(usingFont: font)) / 2
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.changing = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(Animation.linear(duration:self.duration).delay(self.delay).repeatForever(autoreverses: false)) {
self.animate = self.shouldAnimate
}
}
}
}
}
I want to make every button change its colour when I tap on it and change colour back when I tap again. So I made a boolean didTap and according to its value, it should change its background.
The code at the moment changes every button background. As I understand from other posts, this system for Buttons is not working perfectly.
What to do? How to get the result i need? Should I use something else(not Button) ? Should I use something like this didTapB1 and so on?(Seems its gonna be a long code if using that?)
import SwiftUI
var headLine = ["B", "I", "N", "G", "O"]
var numB = ["5","9","11","15","9"]
var numI = ["16","19","21","25","22"]
var numN = ["35","39","41","45","42"]
var numG = ["55","59","61","57","52"]
var numO = ["66","69","71","75","72"]
struct TestView: View {
#State private var didTap:Bool = false
var body: some View {
ZStack {
Color.orange
.edgesIgnoringSafeArea(.all)
HStack {
VStack {
Text("B")
ForEach(numB, id: \.self) { tekst in
Button(action: {
if self.didTap == false {
self.didTap = true
} else {
self.didTap = false
}
}) {
Text(tekst)
.padding()
.background(self.didTap ? Color.red : Color.black)
.clipShape(Circle())
}
}
}
VStack {
Text("I")
ForEach(numI, id: \.self) { tekst in
Button(action: {
if self.didTap == false {
self.didTap = true
} else {
self.didTap = false
}
}) {
Text(tekst)
.padding()
.background(self.didTap ? Color.red : Color.black)
.clipShape(Circle())
}
}
}
VStack {
Text("N")
ForEach(numN, id: \.self) { tekst in
Button(action: {
if self.didTap == false {
self.didTap = true
} else {
self.didTap = false
}
}) {
Text(tekst)
.padding()
.background(self.didTap ? Color.red : Color.black)
.clipShape(Circle())
}
}
}
VStack {
Text("G")
ForEach(numG, id: \.self) { tekst in
Button(action: {
if self.didTap == false {
self.didTap = true
} else {
self.didTap = false
}
}) {
Text(tekst)
.padding()
.background(self.didTap ? Color.red : Color.black)
.clipShape(Circle())
}
}
}
VStack {
Text("O")
ForEach(numO, id: \.self) { tekst in
Button(action: {
if self.didTap == false {
self.didTap = true
} else {
self.didTap = false
}
}) {
Text(tekst)
.padding()
.background(self.didTap ? Color.red : Color.black)
.clipShape(Circle())
}
}
}
}
}
}
}
You need to add didTap to every Button. You can simply do it by creating custom view:
struct BingoButton: View {
var text: String
#State private var didTap = false
var body: some View {
Button(action: {
self.didTap.toggle()
}) {
Text(text)
.padding()
.background(didTap ? Color.red : Color.black)
.clipShape(Circle())
}
}
}
And then you can change your implementation to something like this:
VStack {
Text("I")
ForEach(numI, id: \.self) { tekst in
BingoButton(text: tekst)
}
}
}
You can consider changing your model and make your UI definition smaller and non-repetitive:
struct BingoRow: Identifiable {
let id = UUID()
let headline: String
let numbers: [String]
}
struct SimpleView: View {
var rows = [
BingoRow(headline: "B", numbers: ["5","9","11","15","9"]),
BingoRow(headline: "I", numbers: ["16","19","21","25","22"]),
BingoRow(headline: "N", numbers: ["35","39","41","45","42"]),
BingoRow(headline: "G", numbers: ["55","59","61","57","52"]),
BingoRow(headline: "O", numbers: ["66","69","71","75","72"])
]
var body: some View {
HStack {
ForEach(rows) { row in
VStack {
Text(row.headline)
ForEach(row.numbers, id: \.self) { text in
BingoButton(text: text)
}
}
}
}
}
}