Can't create a simple selectable tag view in SwiftUI - collections

I'm not so familiar with SwfitUI.
I found this helper to achieve this tag view:
But making it selectable is pain & for everything I did, I got many errors...
How can I make this thing work!?
Here is my complete class:
import SwiftUI
struct TagViewItem: Hashable {
var title: String
var isSelected: Bool
static func == (lhs: TagViewItem, rhs: TagViewItem) -> Bool {
return lhs.isSelected == rhs.isSelected
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
hasher.combine(isSelected)
}
}
struct TagView: View {
#State var tags: [TagViewItem]
#State private var totalHeight = CGFloat.zero // << variant for ScrollView/List // = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(tags.indices) { index in
item(for: tags[index].title, isSelected: &tags[index].isSelected)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width) {
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String, isSelected: inout Bool) -> some View {
Text(text)
.foregroundColor(isSelected ? Colors.primaryBarBackground : Colors.textColor)
.padding()
.lineLimit(1)
.background(isSelected ? Colors.primaryBlue : Colors.primaryBarBackground)
.frame(height: 36)
.cornerRadius(18)
.overlay(Capsule().stroke(Colors.primaryBlue, lineWidth: 4))
.onTapGesture {
isSelected.toggle()
}
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}

Finally, after wrestling with it a lot, it is the working version:
struct TagView: View {
#State var tags: [TagViewItem]
#State private var totalHeight = CGFloat.zero // << variant for ScrollView/List // = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(tags.indices) { index in
item(for: tags[index].title, isSelected: tags[index].isSelected)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width) {
width = 0
height -= d.height
}
let result = width
if tags[index].title == self.tags.last!.title {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tags[index].title == self.tags.last!.title {
height = 0 // last item
}
return result
}).onTapGesture {
tags[index].isSelected.toggle()
}
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String, isSelected: Bool) -> some View {
Text(text)
.foregroundColor(isSelected ? Colors.primaryBarBackground : Colors.textColor)
.padding()
.lineLimit(1)
.background(isSelected ? Colors.primaryBlue : Colors.primaryBarBackground)
.frame(height: 36)
.cornerRadius(18)
.overlay(Capsule().stroke(Colors.primaryBlue, lineWidth: 1))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
Usage:
TagView(tags: [TagViewItem(title: "ff", isSelected: false), TagViewItem(title: "yyhuuuh", isSelected: false), TagViewItem(title: "kjhgdtfyughuihu", isSelected: true), TagViewItem(title: "nbyvyvuyv", isSelected: false)])

Related

Is it possible to create a LazyVGrid without the Lazy modifier?

I'm loading data in from my Firebase backend, the "lazy" part makes my app look glitchy/frozen-like when scrolling down, it lags heavily...
Is it possible to create a VGrid "without the lazy functionality"??
(iOS 14)
If not, any suggestions other than ditching the Grid look altogether?
let layout = [
GridItem(.flexible()),
GridItem(.flexible()),
]
#ObservedObject var homeModel = Home_ViewModel()
NavigationView(content: {
ScrollView() {
LazyVGrid(columns: layout, spacing: 10) {
ForEach(homeModel.projectList) { item in
ProjectItemWidget(
projectID: item.id,
projectTitle: item.projectTitle,
projectAuthorProfileImage: item.authorProfileImageUrl,
projectAuthor: item.projectAuthor)
}
}
.padding(.trailing, 7.5)
}
}
I was struggling thinking in a solution where a could create a grid layout without using LazyVGrid and came up with the following:
extension Array {
func getElementAt(index: Int) -> Element? {
return (index < self.endIndex) ? self[index] : nil
}
}
struct CustomGridLayout<Element, GridCell>: View where GridCell: View {
private var array: [Element]
private var numberOfColumns: Int
private var gridCell: (_ element: Element) -> GridCell
init(_ array: [Element], numberOfColumns: Int, #ViewBuilder gridCell: #escaping (_ element: Element) -> GridCell) {
self.array = array
self.numberOfColumns = numberOfColumns
self.gridCell = gridCell
}
var body: some View {
Grid {
ForEach(Array(stride(from: 0, to: self.array.count, by: self.numberOfColumns)), id: \.self) { index in
GridRow {
ForEach(0..<self.numberOfColumns, id: \.self) { j in
if let element = self.array.getElementAt(index: index + j) {
self.gridCell(element)
}
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
An example using in a View:
struct ContentView: View {
private var array: [Int] = Array(1...7)
var body: some View {
CustomGridLayout(array, numberOfColumns: 3) { element in
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.orange)
.overlay(alignment: .center) {
Text("\(element)")
}
}
.padding(.horizontal)
}
}
You can see the result in the following link: https://i.stack.imgur.com/1o7ip.png

Abrupt transition between views within a Navigation View

I'm having trouble figuring out how to smoothly navigate from my SignInView() to my FirstView(). I have my FirstView() inside of a Navigation Stack, but the transition between the views is very abrupt and devoid of the transition that you normally get with the use of a NavigationLink. How can I get the transition to work?
Much appreciated!
Here is the relevant code...
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
VStack{
NavigationView {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.navigationBarHidden(true)
}
}
.onAppear {
viewModel.listen()
}
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
withAnimation {
self.signedIn = true
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
}
struct SignInView: View {
#EnvironmentObject var viewModel: AppViewModel
#State private var username : String = ""
#State private var password : String = ""
#State private var shouldShowLoginAlert: Bool = false
#State var selectedImageArray : [Image] = []
var disableLoginButton : Bool {
return self.username.isEmpty || self.password.isEmpty
}
var body: some View {
VStack{
Image(uiImage: #imageLiteral(resourceName: "awText"))
.resizable()
.frame(width: 180, height: 100)
.padding(.bottom, 50)
TextField("Email", text: $username)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center).padding(.bottom)
.padding(.bottom)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(20)) //Max 10 Characters for Username.
}
})
SecureField("Password", text: $password)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(10)) //Max 10 Characters for Password.
}
})
//SignIn Button
Button(action: {
viewModel.signIn(email: username, password: password)
}, label: {
Text("Sign In")
.disabled(disableLoginButton)
.frame(width: 300, height: 50)
.background(Color.green)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.padding()
})
}
Replacing the default NavigationView behavior with your own animations isn't necessarily totally straightforward. I'll lay out one possibility, but another would be to use a real NavigationView transition, but just hide the back button once you're on FirstView.
To do the transition yourself, you'll need one root element to NavigationView, an if clause, a transition(.slide) and withAnimation. Here's a simplified version of your code showing just these elements:
class AppViewModel: ObservableObject {
#Published var signedIn = false
}
struct FirstView : View {
var body: some View {
Text("Signed in")
}
}
struct ContentView: View {
#StateObject var viewModel = AppViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
Button("Sign me in") {
withAnimation {
viewModel.signedIn = true
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarHidden(true)
}
}
}

swiftUI - Button Camera in TabBar

There is a button in the TabBar, this button should open the camera
"Button(action: {...}, label: { ..."
The camera code is written in "CameraView" and "CameraModel" :
struct CameraView: View {
#StateObject var camera = CameraModel()
var body: some View {
ZStack{
// Camera preview...
CameraPreview(camera: camera)
.ignoresSafeArea(.all, edges: .all)
VStack{
if camera.isTaken{
HStack {
Spacer()
Button(action: {}, label: {
Image(systemName: "arrow.triangle.2.circlepath.camera")
.foregroundColor(.black)
.padding()
.background(Color.white)
.clipShape(Circle())
})
.padding(.trailing,10)
}
}
Spacer()
HStack{
if camera.isTaken{
Button(action: {}, label: {
Text("Save")
.foregroundColor(.black)
.fontWeight(.semibold)
.padding(.vertical,10)
.padding(.horizontal,20)
.background(Color.white)
.clipShape(Capsule())
})
.padding(.leading)
Spacer()
}
else{
Button(action: {camera.isTaken.toggle()}, label: {
ZStack{
Circle()
.fill(Color.white)
.frame(width: 65, height: 65)
Circle()
.stroke(Color.white,lineWidth: 2)
.frame(width: 75, height: 75)
}
})
}
}
.frame(height: 75)
}
}
.onAppear(perform: {
camera.Check()
})
}
}
class CameraModel: ObservableObject{
#Published var isTaken = false
#Published var session = AVCaptureSession()
#Published var alert = false
// since were going to read pic data....
#Published var output = AVCapturePhotoOutput()
// preview....
#Published var preview : AVCaptureVideoPreviewLayer!
func Check(){
// first checking camerahas got permission...
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setUp()
return
// Setting Up Session
case .notDetermined:
// retusting for permission....
AVCaptureDevice.requestAccess(for: .video) { (status) in
if status{
self.setUp()
}
}
case .denied:
self.alert.toggle()
return
default:
return
}
}
func setUp(){
// setting up camera...
do{
// setting configs...
self.session.beginConfiguration()
// change for your own...
let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
let input = try AVCaptureDeviceInput(device: device!)
// checking and adding to session...
if self.session.canAddInput(input){
self.session.addInput(input)
}
// same for output....
if self.session.canAddOutput(self.output){
self.session.addOutput(self.output)
}
self.session.commitConfiguration()
}
catch{
print(error.localizedDescription)
}
}
}
// setting view for preview...
struct CameraPreview: UIViewRepresentable {
#ObservedObject var camera : CameraModel
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
// Your Own Properties...
camera.preview.videoGravity = .resizeAspectFill
view.layer.addSublayer(camera.preview)
// starting session
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
How to call the camera on click Button in TabBar "Button(action: {}, label: {
)"?
This is just an example of how I did it in the past (after all permissions etc... have been done).
Use your code to do the same.
struct ContentView: View {
#State var image: UIImage?
#State private var showCamera = false
var body: some View {
VStack {
Button(action: { self.showCamera.toggle() }) {
Image(systemName: "camera.circle").resizable().frame(width: 100, height: 100)
}
if image != nil {
Image(uiImage: image!).resizable().frame(width: 200, height: 200)
}
} // this is where it happens
.sheet(isPresented: $showCamera, onDismiss: {self.showCamera = false}) {
CameraViewController(photo: $image)
}
}
}
struct CameraViewController: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photo: UIImage?
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewController>) -> UIImagePickerController {
let vc = UIImagePickerController()
vc.sourceType = .camera
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, AVCapturePhotoCaptureDelegate {
var parent: CameraViewController
var captureSession: AVCaptureSession!
var capturePhotoOutput: AVCapturePhotoOutput!
var theCamera: AVCaptureDevice!
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
let photoQualityPrioritizationMode = AVCapturePhotoOutput.QualityPrioritization.speed
init(_ imagePickerController: CameraViewController) {
self.parent = imagePickerController
}
// called when a picture has been taken
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info:[UIImagePickerController.InfoKey : Any]) {
guard let image = info[.originalImage] as? UIImage else {
print("No image found")
return
}
parent.photo = image // <--- the photo image
parent.presentationMode.wrappedValue.dismiss()
}
}
}

swiftUI how to disabled a button for 2sec?

i have a bug, if i click on button before the animation before the card flip back. i think for me the best it would be to disable the button for 2 sec, but i made some research and didnt find anything!
struct CardBack: View {
var body: some View {
Image("back_card")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
}
}
struct ContentView: View {
#State var flipped = false
#State private var cardsFront = ["bigCard1", "bigCard2", "bigCard3", "bigCard4", "bigCard5" ]
#State private var cardBack = "back_card"
#State private var disablled = true
var body: some View {
VStack {
Spacer()
ZStack {
Image(flipped ? self.cardsFront.randomElement()! : self.cardBack)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
.rotation3DEffect(Angle(degrees: flipped ? 180 : 0 ), axis: (x: 0, y: 1, z: 0))
}
Spacer()
HStack {
Button(action: {
withAnimation(.spring()) {
self.flipped.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation(.spring()) {
self.flipped.toggle()
}
}
}) {
Image("circle")
.renderingMode(.original)
}
Button(action: {
}) {
Image("plus")
.renderingMode(.original)
}
iOS 13, Swift 5
You can set the button as disabled initially and then enable it using the same sort of logic I used here.
import SwiftUI
struct ContentView: View {
#State var changeColor = false
var body: some View {
TextView(changeColor: $changeColor)
}
}
struct TextView: View {
#Binding var changeColor: Bool
var body: some View {
Text("Hello World")
.foregroundColor(changeColor ? Color.black: Color.red)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.changeColor.toggle()
}
}
}
}
You are almost there, you just need to use the .appear tag in your code to do this.

SWIFTUI Call Key Dictionary not work with the error: 'Subscript index of type '() -> Bool' in a key path must be Hashable'

I have this view:
import SwiftUI
struct SectionView1: View {
let dateStr:String
#Binding var isSectionView:Bool
var body: some View {
HStack {
Button(action: {
self.isSectionView.toggle()
}) {
Image(systemName: isSectionView ? "chevron.down.circle" : "chevron.right.circle")
}
Text("Media del \(dateStr)")
}
}
}
which will be called from view:
import SwiftUI
import Photos
struct MediaView: View {
let geoFolder:GeoFolderCD
#State private var assetsForDate = [String :[PHAsset]]()
#State private var isSectionViewArray:[String:Bool] = [:]
var body: some View {
List {
ForEach(assetsForDate.keys.sorted(by: > ), id: \.self) { dateStr in
Section {
SectionView1(dateStr: dateStr,
isSectionView: self.$isSectionViewArray[dateStr, default: true])
}
}
}
.onAppear {
self.assetsForDate = FetchMediaUtility().fetchGeoFolderAssetsForDate(geoFolder: geoFolderStruct, numAssets: numMediaToFetch)
for dateStr in self.assetsForDate.keys.sorted() {
self.isSectionViewArray[dateStr] = true
}
}
}
}
but I have the error: Subscript index of type '() -> Bool' in a key path must be Hashable in isSectionView: self.$isSectionViewArray[dateStr, default: true]
Why isSectionViewArray:[String:Bool] = [:] is not Hasbable?
How can modify the code for work?
If I remove, in SectionView, #Binding var isSectionView:Bool, the code work fine, or if I set, from SectionView, #Binding var isSectionViewArray:[String:Bool] = [:], the code work fine.
You can write your own binding with the below code and it should work
var body: some View {
List {
ForEach(assetsForDate.keys.sorted(by: > ), id: \.self) { dateStr in
let value = Binding<Bool>(get: { () -> Bool in
return self.isSectionViewArray[dateStr, default: true]
}) { (value) in
}
Section {
SectionView1(dateStr: dateStr,
isSectionView: value)
}
}
}
.onAppear {
self.assetsForDate = FetchMediaUtility().fetchGeoFolderAssetsForDate(geoFolder: geoFolderStruct, numAssets: numMediaToFetch)
for dateStr in self.assetsForDate.keys.sorted() {
self.isSectionViewArray[dateStr] = true
}
}
}

Resources