Need to fix tooltip view position - swiftui

Here I want to show the tooltip on of tap in the button. The position of the tooltip should be just the top of the TooTipButton and should have the dynamic height to the message text. It is used in many places in 'HStack', 'VStack's with the other content. It should fit in the layout.
For example here the code has HStack inside that Text and TooTipButton. But it does not show the Text component with the text Created by Me
import SwiftUI
import Combine
struct ContentView: View {
#State var showing = false
var body: some View {
GeometryReader { reader in
VStack {
HStack {
Text("Created by Me")
TooTipButton(message: "hey.!! here is some view.", proxy: reader)
}
Spacer()
}
}
}
}
struct ToolTipMessage<Presenting>: View where Presenting: View {
#Binding var isShowing: Bool
let presenting: () -> Presenting
let text: String
let proxy: GeometryProxy
var body: some View {
ZStack(alignment: .center) {
self.presenting()
Group {
Text(text)
.font(.body)
.padding(.horizontal)
.padding(.vertical, 8)
.multilineTextAlignment(.center)
.foregroundColor(.white)
.shadow(radius: 6)
.background(Color(.systemBlue).opacity(0.96) )
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
}.frame(width: proxy.size.width)
}
.onReceive(Just(isShowing)) { shwoing in
if shwoing {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.isShowing = false
}
}
}
.onTapGesture {
self.isShowing = false
}
}
}
extension View {
func toast(isShowing: Binding<Bool>, text: String, proxy: GeometryProxy) -> some View {
ToolTipMessage(isShowing: isShowing,
presenting: { self },
text: text,
proxy: proxy)
}
}
struct TooTipButton: View {
let message: String
#State var showMessage = false
let proxy: GeometryProxy
var body: some View {
Button {
showMessage = true
} label: {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.red)
}
.toast(isShowing: $showMessage, text: message, proxy: proxy)
}
}
Here are the images:

Using .overlay and .offset should help:
struct ToolTipMessage<Presenting>: View where Presenting: View {
#Binding var isShowing: Bool
let presenting: () -> Presenting
let text: String
let proxy: GeometryProxy
var body: some View {
ZStack(alignment: .center) {
self.presenting()
.overlay {
Text(text)
.font(.body)
.padding(.horizontal)
.padding(.vertical, 8)
.multilineTextAlignment(.center)
.foregroundColor(.white)
.shadow(radius: 6)
.background(Color(.systemBlue).opacity(0.96) )
.cornerRadius(20)
.transition(.slide)
.opacity(self.isShowing ? 1 : 0)
.frame(width: proxy.size.width)
.offset(x: 0, y: -30)
}
}
.onReceive(Just(isShowing)) { shwoing in
if shwoing {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.isShowing = false
}
}
}
.onTapGesture {
self.isShowing = false
}
}
}

Related

How to move up SwiftUI `ScrollView` content when keyboard is appeared?

I'm having a chat view with messages. When message composer gets a focus and keyboard is appeared the height of ScrollView decreases. Now I want all messages to move up a little so users can see the same bottom message she saw before. Is there anyway to achieve this with a pure SwiftUI?
ScrollViewReader { scrollReader in
ScrollView {
LazyVStack(spacing: 24) {
ForEach(messages, id: \.id) {
MessageContainer(message: $0)
.id($0.id)
}
}
.padding(.horizontal, 16)
}
}
Here is an example that uses ScrollViewReader to scroll to the tapped message for answering it:
struct ContentView: View {
let messages = Message.dummyData
#State private var tappedMessage: Message?
#State private var newMessage = ""
#FocusState private var focus: Bool
var body: some View {
ScrollViewReader { scrollReader in
ScrollView {
LazyVStack(alignment: .leading, spacing: 24) {
ForEach(messages, id: \.id) { message in
MessageContainer(message: message)
.id(message.id)
.onTapGesture {
tappedMessage = message
focus = true
}
}
}
.padding(.horizontal, 16)
}
if let tappedMessage {
VStack {
TextEditor(text: $newMessage)
.frame(height: 80)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.gray, lineWidth: 1)
)
.padding(.horizontal, 16)
.focused($focus)
Button("Send") { self.tappedMessage = nil }
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
withAnimation {
scrollReader.scrollTo(tappedMessage.id)
}
}
}
}
}
}
}
Then just try it with .offset:
struct ContentView: View {
let messages = Message.dummyData
#State private var showNewMessage = false
#State private var newMessage = ""
#FocusState private var focus: Bool
var body: some View {
VStack {
ScrollView {
LazyVStack(alignment: .leading, spacing: 24) {
ForEach(messages, id: \.id) { message in
MessageContainer(message: message)
.offset(y: showNewMessage ? -300 : 0)
}
}
.padding(.horizontal, 16)
}
if showNewMessage == false {
Button("New Message") {
withAnimation {
showNewMessage = true
focus = true
}
}
} else {
Button("Send") {
showNewMessage = false
}
TextEditor(text: $newMessage)
.frame(height: 80)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.gray, lineWidth: 1)
)
.padding(.horizontal, 16)
.focused($focus)
}
}
}
}

Why the scrollview doesn't get updated with new data from array?

I'm trying to send and then display them in the scrollview realtime. But nothing shows up. How to solve it? So, basically when the user types the message into a textbox then it will be saved in array and then it will be populated to the crollView in realtime so the user can view all the messages.
Error: No errors, it just isn't visible.
import SwiftUI
struct SingleMessageBubbleModel: Identifiable {
let id = UUID()
var text: String
var received: Bool
var timeStamp: Date
}
var messagesDBArray : [SingleMessageBubbleModel] = []
struct ContentView: View {
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
struct MessageBubble: View {
var message: SingleMessageBubbleModel
#State private var showTime = false
var body: some View {
VStack(alignment: message.received ? .leading : .trailing) {
HStack {
Text(message.text)
.padding()
.background(message.received ? Color.gray : Color.blue)
.cornerRadius(30)
}
.frame(maxWidth: 300, alignment: message.received ? .leading : .trailing)
.onTapGesture {
withAnimation {
showTime.toggle()
}
}
if showTime {
Text("\(message.timeStamp.formatted(.dateTime.hour().minute()))")
.font(.caption2)
.foregroundColor(.gray)
.padding(message.received ? .leading : .trailing, 25)
}
}
.frame(maxWidth: .infinity, alignment: message.received ? .leading : .trailing)
.padding(message.received ? .leading : .trailing)
.padding(.horizontal, 4)
}
}
Basically, when the button is pressed, your property messagesDBArray is well and truly append with the new value.
However, and it's really important to understand this point in swiftUI, nothing triggers the refresh of the view.
I suggest you two solutions:
If you don't need messagesDBArray to be outside of ContentView:
You just have to add messagesDBArray as a state in ContentView like following
struct ContentView: View {
#State var messagesDBArray : [SingleMessageBubbleModel] = []
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
If you need messagesDBArray to be outside of ContentView:
1- Create a class (ViewModel or Service or whatever you wan to call it) with messagesDBArray as a #Published property
final class ViewModel: ObservableObject {
#Published var messagesDBArray : [SingleMessageBubbleModel] = []
}
2- Observe this class in ContentView in order to append and receive the update
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(viewModel.messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: viewModel.messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
viewModel.messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
I hope that this is clear to you and that it has been useful 😉

How can I have Image and Text for Picker in SegmentedPickerStyle in SwiftUI?

I am building a Picker with SwiftUI.
Now i want do add an icon AND text for each selection. So it should look something like this:
Is this possible? If yes how to do it?
Or is it not recommended by Apples apples human interface guidelines at all?
I already tried to use a HStack to wrap image and text together.
enum Category: String, CaseIterable, Identifiable {
case person
case more
var id: String { self.rawValue }
}
struct ContentView: View {
#State private var category = Category.person
var body: some View {
Picker("Category", selection: $category) {
HStack {
Image(systemName: "person")
Text("Person")
}.tag(Category.person)
HStack {
Image(systemName: "ellipsis.circle")
Text("More")
}.tag(Category.more)
}.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
But the framework splits it up into four.
You can make a custom Picker
struct ContentView: View {
var body: some View {
Home()
}
}
struct Home: View {
#State var index = 0
var body: some View {
VStack {
HStack {
Text("Picker with icon")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.black)
Spacer(minLength: 0)
}
.padding(.horizontal)
HStack(spacing: 0){
HStack{
Image(systemName: "person")
.foregroundColor(self.index == 0 ? .black : .gray)
Text("Person")
.foregroundColor(self.index == 0 ? .black : .gray)
}
.padding(.vertical, 10)
.padding(.horizontal, 35)
.background((Color.white).opacity(self.index == 0 ? 1 : 0))
.clipShape(Capsule())
.onTapGesture {
self.index = 0
}
HStack{
Image(systemName: "ellipsis.circle")
.foregroundColor(self.index == 1 ? .black : .gray)
Text("More")
.foregroundColor(self.index == 1 ? .black : .gray)
}
.padding(.vertical, 10)
.padding(.horizontal, 35)
.background((Color.white).opacity(self.index == 1 ? 1 : 0))
.clipShape(Capsule())
.onTapGesture {
self.index = 1
}
}
.padding(3)
.background(Color.black.opacity(0.06))
.clipShape(Capsule())
Spacer(minLength: 0)
}
.padding(.top)
}
}
This is a way using Apple Picker with the output you want:
enum Category: String, CaseIterable, Identifiable {
case person
case more
var id: String { self.rawValue }
}
struct ContentView: View {
#State private var category = Category.person
private var view1: some View { HStack { Image(systemName: "person"); Text("Person") } }
private var view2: some View { HStack { Image(systemName: "ellipsis.circle"); Text("More") } }
#State private var uiImage1: UIImage? = nil
#State private var uiImage2: UIImage? = nil
var body: some View {
return Picker("Category", selection: $category) {
if let unwrappedUIImage1 = uiImage1 {
Image(uiImage: unwrappedUIImage1)
.tag(Category.person)
}
if let unwrappedUIImage2 = uiImage2 {
Image(uiImage: unwrappedUIImage2)
.tag(Category.more)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
.onAppear() {
DispatchQueue.main.async {
uiImage1 = viewToUIImageConverter(content: view1)
uiImage2 = viewToUIImageConverter(content: view2)
}
print("Your selection is:", category.rawValue)
}
.onChange(of: category, perform: { newValue in print("Your selection is:", newValue.rawValue) })
}
}
func viewToUIImageConverter<Content: View>(content: Content) -> UIImage? {
let controller = UIHostingController(rootView: content)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = UIColor.clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}

Result of 'View' initializer is unused

My Custom button does not tap and passes to next view called AddCreditCardView.
I have tested the button action with print statement and it won't work too.
I copied my code below in separate.
This is my ContentView
import SwiftUI
struct ContentView: View {
let membershipRows = MembershipData.listData()
let corporateRows = CorporateData.listData()
let otherOperationRows = OtherOperationsData.listData()
#State var selectedCard = CreditCard(id: "", cardOwnerName: "", cardNumber: "", cardExpMonth: "", cardExpYear: "", ccv: "")
#State var shown: Bool = false
var body: some View {
NavigationView {
VStack {
List {
Section(header: Text("Bireysel")) {
ForEach(membershipRows) { row in
NavigationLink(destination: CreditCardView()) {
RowElementView(row: row)
}
}
}
if self.corporateRows.count == 0
{
Rectangle()
.background(Color(.white))
.edgesIgnoringSafeArea(.all)
.foregroundColor(.white)
.padding(.vertical,32)
}
else {
Section(header: Text("Kurumsal")) {
ForEach(corporateRows) { row in
RowElementView(row: row)
}
}
}
Section(header: Text("Diger Islemler")) {
ForEach(otherOperationRows) { row in
RowElementView(row: row)
}
}
Rectangle()
.foregroundColor(.clear)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height )
}
.navigationBarTitle("Odeme Yontemleri", displayMode: .inline)
.font(Font.custom("SFCompactDisplay", size: 16))
Button(action: {
AddCreditCardView(item: self.selectedCard)
}, label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable)
})
}
}
}
This is my AddCreditCardView
import SwiftUI
struct AddCreditCardView: View {
var item: CreditCard
var body: some View {
NavigationView {
VStack {
TopBar()
Spacer()
CardInfo()
Spacer()
}
.navigationBarTitle("Odeme Yontemi", displayMode: .inline)
}
}
}
struct TopBar : View {
var body: some View {
VStack {
HStack() {
Image("addcreditcard")
Image("line")
Image("locationBar")
Image("line")
Image("check-circle")
}
.padding(.horizontal,62)
VStack {
Text("Kredi Karti Ekle")
.font(Font.custom("SFCompactDisplay-Bold", size: 14))
Text("1. Adim")
.font(Font.custom("SFCompactDisplay", size: 14))
.fontWeight(.regular)
.foregroundColor(.gray)
}
}
.padding()
}
}
struct CardInfo : View {
var body: some View {
VStack {
CustomTextField(tFtext: "Kartin Uzerindeki Isim", tFImage: "user")
.textContentType(.givenName)
CustomTextField(tFtext: "Kredi Kart Numarasi", tFImage: "credit")
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
HStack {
CreditCardDateTextField(tFtext: "", tFImage: "date")
.textContentType(.creditCardNumber)
Spacer()
Text("|")
.foregroundColor(.black)
.overlay(
Rectangle()
.frame(width: 60, height: 53))
CustomTextField(tFtext: "CCV", tFImage: "")
.textContentType(.creditCardNumber)
}
.foregroundColor(Color(#colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9882352941, alpha: 1)))
CustomTextField(tFtext: "Kart Ismi", tFImage: "cardEdit")
Spacer()
}
}
}
And Finally, this is my CreditCard Model
import SwiftUI
struct CreditCard: Identifiable {
var id: String = UUID().uuidString
var cardOwnerName : String
var cardNumber: String
var cardExpMonth: String
var cardExpYear: String
var ccv: String
Seems like you are trying to navigate to AddCreditCardView on the button press. The action closure can not present a view automatically like that! You should change that code to something like this:
#State var navigated = false
,,,
NavigationLink("AddCreditCardView", destination: AddCreditCardView(), isActive: $navigated)
Button(action: { self.navigated.toggle() },
label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable) })
changing the navigated state will show the next page as it seems you wished.

Using SwiftUI. My Slider/Side-menu launches new Views just fine when clicked but click <back> button and now all the options are 'dead'

Using SwiftUI and a slider/side menu tutorial that I have augmented in order to put actions on each of the side menu selections.
When the side menu is displayed and I tap a menu option, it works great and takes me to a new view with a menu item. But when i tap on and see the side menu still in place, all the menu items are not dead. The menu items still animate a click (with a flicker) but nothing happens. I have to close the side menu, reopen it, and then the menu items work once again - one time.
Can anyone tell me why this is happening?
Here is the pretty contentview, the mainview, and the sidemenu view.
//ContentView.swift
import SwiftUI
struct ContentView: View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
.navigationBarTitle("Side Menu", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}
}
struct MainView: View {
#Binding var showMenu: Bool
var body: some View {
Button(action: {
withAnimation {
self.showMenu = true
}
}) {
Text("Show Menu")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and here is the sidemenu view.
//MenuView.swift
import SwiftUI
struct PlayerView: View {
#State var showMenu = true
//#EnvironmentObject var session: SessionStore
var body: some View {
VStack{
//self.showMenu = true
Text("Manage Players Here").foregroundColor(.red)
}
}
}
struct MenuView: View {
#State var showMenu = true
var body: some View {
VStack(alignment: .leading) {
HStack() {
NavigationLink(destination: PlayerView()) {
HStack(){
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Players")
.foregroundColor(.gray)
.font(.headline)
}
}
}
.padding(.top, 100)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}
struct MenuView_Previews: PreviewProvider {
static var previews: some View {
MenuView()
}
}
enter code here
1) Binding var in the MenuView
2) OnAppear{} with Zstack to turn off the showMenu
struct ContentView: View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView(showMenu: self.$showMenu)
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag).onAppear {
self.showMenu = false
}
}
.navigationBarTitle("Side Menu", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}
}
struct MainView: View {
#Binding var showMenu: Bool
var body: some View {
Button(action: {
withAnimation {
self.showMenu = true
}
}) {
Text("Show Menu")
}
}
}
struct PlayerView: View {
#State var showMenu = true
//#EnvironmentObject var session: SessionStore
var body: some View {
VStack{
//self.showMenu = true
Text("Manage Players Here").foregroundColor(.red)
}
}
}
struct MenuView: View {
#Binding var showMenu: Bool // = true
var body: some View {
VStack(alignment: .leading) {
HStack() {
NavigationLink(destination: PlayerView()) {
HStack(){
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Players")
.foregroundColor(.gray)
.font(.headline)
}
}
}
.padding(.top, 100)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(red: 32/255, green: 32/255, blue: 32/255))
.edgesIgnoringSafeArea(.all)
}
}