I usually work with Obj-C but I have 2 .swift files that I have imported into my project, what I am looking to do is launch one of the files, from one of my Obj-C viewController files (which subsequently is attached to a xib)
-(IBAction)gotoCompass:(id)sender {
let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen //or .overFullScreen for transparency
self.present(vc, animated: true, completion: nil)
}
That is the action I want to use, the .swift file names are
ContentView.swift & CompassHeading.swift
I would like to present one of them using the IBAction above, but currently it shows
'Use of undeclared identifier 'let'
I have imported the Bridging-Header file and the top of the .m file is
FirstViewController.m
#interface FirstViewController ()
#end
#implementation FirstViewController
Edit -
Here is the contents of ContentView.swift
import Foundation
import SwiftUI
struct Marker: Hashable {
let degrees: Double
let label: String
init(degrees: Double, label: String = "") {
self.degrees = degrees
self.label = label
}
func degreeText() -> String {
return String(format: "%.0f", self.degrees)
}
static func markers() -> [Marker] {
return [
Marker(degrees: 0, label: "N"),
Marker(degrees: 30),
Marker(degrees: 60),
Marker(degrees: 90, label: "E"),
Marker(degrees: 120),
Marker(degrees: 150),
Marker(degrees: 180, label: "S"),
Marker(degrees: 210),
Marker(degrees: 240),
Marker(degrees: 270, label: "W"),
Marker(degrees: 300),
Marker(degrees: 330)
]
}
}
struct CompassMarkerView: View {
let marker: Marker
let compassDegress: Double
var body: some View {
VStack {
Text(marker.degreeText())
.fontWeight(.light)
.rotationEffect(self.textAngle())
Capsule()
.frame(width: self.capsuleWidth(),
height: self.capsuleHeight())
.foregroundColor(self.capsuleColor())
Text(marker.label)
.fontWeight(.bold)
.rotationEffect(self.textAngle())
.padding(.bottom, 180)
}.rotationEffect(Angle(degrees: marker.degrees))
}
private func capsuleWidth() -> CGFloat {
return self.marker.degrees == 0 ? 7 : 3
}
private func capsuleHeight() -> CGFloat {
return self.marker.degrees == 0 ? 45 : 30
}
private func capsuleColor() -> Color {
return self.marker.degrees == 0 ? .red : .gray
}
private func textAngle() -> Angle {
return Angle(degrees: -self.compassDegress - self.marker.degrees)
}
}
struct ContentView : View {
#ObservedObject var compassHeading = CompassHeading()
var body: some View {
VStack {
Capsule()
.frame(width: 5,
height: 50)
ZStack {
ForEach(Marker.markers(), id: \.self) { marker in
CompassMarkerView(marker: marker,
compassDegress: self.compassHeading.degrees)
}
}
.frame(width: 300,
height: 300)
.rotationEffect(Angle(degrees: self.compassHeading.degrees))
.statusBar(hidden: true)
}
}
}
Also there is a CompassHeading.swift file;
import Foundation
import Combine
import CoreLocation
class CompassHeading: NSObject, ObservableObject, CLLocationManagerDelegate {
var objectWillChange = PassthroughSubject<Void, Never>()
var degrees: Double = .zero {
didSet {
objectWillChange.send()
}
}
private let locationManager: CLLocationManager
override init() {
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
self.setup()
}
private func setup() {
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.headingAvailable() {
self.locationManager.startUpdatingLocation()
self.locationManager.startUpdatingHeading()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
self.degrees = -1 * newHeading.magneticHeading
}
}
Related
I'm having an issue while moving a view that contains a SwiftUI view near the edges of the screen. The SwiftUI view moves itself to avoid being blocked by the safe area insets
I'm using UIKit to handle dragging the view via UIHostingController and a UIPanGestureRecognizer. Here's the code
import UIKit
class ViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let contentVc = UIHostingController(rootView: Content())
addChild(contentVc)
contentView = contentVc.view
let contentHeight = contentView.sizeThatFits(view.bounds.size).height
contentView.frame = CGRect(x: 0, y: 200, width: view.bounds.width, height: contentHeight)
view.addSubview(contentView)
contentVc.didMove(toParent: self)
let drag = UIPanGestureRecognizer(target: self, action: #selector(drag(_:)))
contentView.addGestureRecognizer(drag)
view.backgroundColor = .orange
}
var startingPoint = CGPoint.zero
#objc func drag(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
startingPoint = contentView.frame.origin
case .changed:
let location = gesture.translation(in: view)
contentView.frame.origin.y = startingPoint.y + location.y
default:
break
}
}
}
import SwiftUI
struct Content: View {
var body: some View {
VStack {
ForEach(0..<10) { i in
Text(String(i))
}
}
.background(Color.blue)
.padding(.vertical, 32)
.ignoresSafeArea()
}
}
What I'm expecting is the blue view to not to adjust itself vertically (in the preview the blue view's top padding is shrinking)
I found 2 ways for solving this issue:
Way1: Using UIKit Gesture!
class ViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let contentVc = UIHostingController(rootView: Content())
addChild(contentVc)
contentView = contentVc.view
let contentHeight = contentView.sizeThatFits(view.bounds.size).height
contentView.frame = CGRect(x: 0, y: 100, width: view.bounds.width, height: contentHeight)
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentVc.didMove(toParent: self)
let drag = UIPanGestureRecognizer(target: self, action: #selector(drag(_:)))
contentView.addGestureRecognizer(drag)
view.backgroundColor = .orange
}
var startingPoint = CGPoint.zero
#objc func drag(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
startingPoint = contentView.frame.origin
case .changed:
let location = gesture.translation(in: view)
contentView.frame.origin.y = startingPoint.y + location.y
default:
break
}
}
}
import SwiftUI
struct Content: View {
var body: some View {
VStack {
ForEach(0..<10) { i in
Text(String(i))
}
}
.padding()
.background(Color.blue)
.frame(maxWidth: .infinity)
.background(Color.white)
}
}
Way2: Using SwiftUI Gesture!
class ViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let contentVc = UIHostingController(rootView: Content())
addChild(contentVc)
contentView = contentVc.view
contentView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentVc.didMove(toParent: self)
view.backgroundColor = .orange
}
}
struct Content: View {
#State private var offset: CGFloat = .zero
#State private var lastOffset: CGFloat = .zero
var body: some View {
VStack {
ForEach(0..<10) { i in
Text(String(i))
}
}
.padding()
.background(Color.blue)
.frame(maxWidth: .infinity)
.background(Color.white)
.offset(y: offset)
.gesture(DragGesture(minimumDistance: .zero, coordinateSpace: .local).onChanged { value in
offset = lastOffset + value.translation.height
}
.onEnded { value in
lastOffset = lastOffset + value.translation.height
offset = lastOffset
})
}
}
I'm trying to implement a little fun addition into an app I'm working on, and can't seem to figure out how to get the animation to work completely / correctly.
At the moment I have something that looks like this: https://i.imgur.com/95O1Wul.mp4
What I'm trying to achieve
I'm trying to create a hologram like you would see on some shiny game cards or in some bank notes or on those VOID stickers.
I'm using the x position of the device to determine whether to shift the start and end point of the gradient. I'm trying to figure out in my head how to use these values to make the gradient's start and end points shift based on the values. And if I incorporated the other axes how it would move around if the device is rotated.
As for code, at the moment I have:
View Model
final class ContentViewModel: ObservableObject {
#Published var gyroRotation = CMRotationRate()
private let manager = CMMotionManager()
private var timer: Timer?
func startGyroscope() {
if(manager.isGyroAvailable) {
manager.gyroUpdateInterval = 1.0
manager.startGyroUpdates()
self.timer = Timer(fire: Date(), interval: (1.0/60.0), repeats: true, block: { timer in
if let data = self.manager.gyroData {
self.gyroRotation = data.rotationRate
}
})
RunLoop.current.add(self.timer!, forMode: .default)
}
}
func stopGyroscope() {
if(self.timer != nil) {
self.timer?.invalidate()
self.timer = nil
self.manager.stopGyroUpdates()
}
}
}
View
struct ContentView: View {
#StateObject var cm = ContentViewModel()
var rotation: CMRotationRate { cm.gyroRotation }
let gridItems = [
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90))
]
var body: some View {
LazyVGrid(columns: gridItems, spacing: 30) {
ForEach(0..<60) { _ in
LinearGradient(
colors: [.red, .orange, .yellow, .blue, .purple, .green],
startPoint: rotation.x > 0 ? .leading : .trailing,
endPoint: rotation.x > 0 ? .trailing : .leading
)
.animation(.linear, value: rotation.x)
.frame(width: 50, height: 50)
.mask(
Image("logo")
.resizable()
.scaledToFit()
.opacity(0.3)
)
}
}
.padding(20)
.onAppear { cm.startGyroscope() }
}
}
Continuing on this to make it work, and I realised I didn't need the LinearGradient to animate but the Color itself:
View
struct CoreMotion: View {
// -- where the data lives
#ObservedObject var coreMotionVM = CoreMotionViewModel()
let gridItems = [
GridItem(), GridItem(), GridItem(), GridItem()
]
// -- the view
var body: some View {
LazyVGrid(columns: gridItems, spacing: 5) {
ForEach( 0 ..< 60 ) { _ in
coreMotionVM.tintColour
.animation(
.easeInOut,
value: coreMotionVM.roll
)
.frame(width: 60, height: 60)
.padding(.vertical)
.mask(
Image("logo")
.resizable()
.scaledToFit()
.opacity(0.2)
)
}
}
.padding(.horizontal)
.padding(.top)
}
}
ViewModel
final class CoreMotionViewModel: ObservableObject {
// -- what we share
#Published var roll: Double = 0.0
// -- core motion manager
private var manager: CMMotionManager
// -- initialisation
init() {
// -- create the instance
self.manager = CMMotionManager()
// -- how often to get the data
self.manager.deviceMotionUpdateInterval = 1/60
// -- start it on the main thread
self.manager.startDeviceMotionUpdates(to: .main) { motionData, error in
// -- error lets get out of here
guard error == nil else {
print(error!)
return
}
// -- update the data
if let motionData = motionData {
self.roll = motionData.attitude.roll
}
}
}
// cycle to the next point
var tintColour: Color {
switch roll {
case ..<(-1.5):
return .red
case -1.5 ..< -1.0 :
return .orange
case -1.0 ..< -0.5 :
return .yellow
case -0.5 ..< 0.5 :
return .green
case 0.5 ..< 1.0 :
return .blue
case 1.0... :
return .purple
default:
return .pink
}
}
}
I am trying to create a custom SwiftUI View but I am getting problem in updating value through ObservedObject.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var model:InfoViewModel = InfoViewModel()
var body: some View {
VStack(alignment: .leading){
CustomView(value: self.model.title)
.frame(width: 200, height: 200, alignment: .center)
}
.background(Color.green)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CustomView: UIViewRepresentable {
typealias UIViewType = UIView
var value:String
var lblTitle:UILabel = UILabel()
func makeUIView(context: Context) -> UIView {
let view:UIView = UIView()
view.addSubview(lblTitle)
lblTitle.text = value
lblTitle.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
lblTitle.backgroundColor = UIColor.red
lblTitle.textColor = UIColor.black
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
self.lblTitle.text = value
print("LBLTitle:\(self.lblTitle.text!)\nTitleValue:\(self.value)")
}
}
class InfoViewModel: ObservableObject, Identifiable {
#Published var title = "Title"
private var count = 0
init() {
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}
#objc func fireTimer() {
count += 1
self.title = "\(self.count)"
print("Timer fired!\n\(self.title)")
}
}
Project can be download from here
Edit:
I changed code after #Asperi answer now it is updating value. but as Dávid Pásztor suggested it is not right to create #ObservedObject inside struct then how I will pass the value to CustomView with creating #ObservedObject and link that David provided is old and code in that thread now seems not working.
Here is the code that is working
struct CustomView: UIViewRepresentable {
typealias UIViewType = UIView
var value:String
func makeUIView(context: Context) -> UIView {
let view:UIView = UIView()
let lblTitle:UILabel = UILabel()
view.addSubview(lblTitle)
lblTitle.text = value
lblTitle.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
lblTitle.backgroundColor = UIColor.red
lblTitle.textColor = UIColor.black
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
for view in uiView.subviews
{
if(view.isKind(of: UILabel.self))
{
(view as! UILabel).text = value
print("LBLTitle:\((view as! UILabel).text!)\nTitleValue:\(self.value)")
}
}
}
}
You don't need to keep it as property because struct can be recreated (so you loose it), instead you have access your view via provided in arguments.
Note: UIViewRepresentable handles reference to corresponding view by itself.
struct CustomView: UIViewRepresentable {
typealias UIViewType = UILabel
var value:String
func makeUIView(context: Context) -> UILabel {
let lblTitle = UILabel()
lblTitle.text = value
lblTitle.backgroundColor = UIColor.red
lblTitle.textColor = UIColor.black
return lblTitle
}
func updateUIView(_ uiLabel: UILabel, context: Context) {
uiLabel.text = value
}
}
Is there a way, using SwiftUI, to create a Slider with 2 handles?
I'm working on a project that that requires settings a low and high point for a random value to be created between, and sliders seem to fit that need perfectly. I currently have it implemented as 2 separate sliders, but it would much rather have it 1 slider with 2 handles.
I've been searching and I cannot find any examples of it in SwiftUI, but I did find a webpage example of what I'm looking to do here: https://jqueryui.com/slider/#range
Is this possible in iOS via SwiftUI?
I've created a custom slider for you. I hope that's enough for your needs. Let me know if there is anything else I can do.
Slider:
import SwiftUI
import Combine
//SliderValue to restrict double range: 0.0 to 1.0
#propertyWrapper
struct SliderValue {
var value: Double
init(wrappedValue: Double) {
self.value = wrappedValue
}
var wrappedValue: Double {
get { value }
set { value = min(max(0.0, newValue), 1.0) }
}
}
class SliderHandle: ObservableObject {
//Slider Size
let sliderWidth: CGFloat
let sliderHeight: CGFloat
//Slider Range
let sliderValueStart: Double
let sliderValueRange: Double
//Slider Handle
var diameter: CGFloat = 40
var startLocation: CGPoint
//Current Value
#Published var currentPercentage: SliderValue
//Slider Button Location
#Published var onDrag: Bool
#Published var currentLocation: CGPoint
init(sliderWidth: CGFloat, sliderHeight: CGFloat, sliderValueStart: Double, sliderValueEnd: Double, startPercentage: SliderValue) {
self.sliderWidth = sliderWidth
self.sliderHeight = sliderHeight
self.sliderValueStart = sliderValueStart
self.sliderValueRange = sliderValueEnd - sliderValueStart
let startLocation = CGPoint(x: (CGFloat(startPercentage.wrappedValue)/1.0)*sliderWidth, y: sliderHeight/2)
self.startLocation = startLocation
self.currentLocation = startLocation
self.currentPercentage = startPercentage
self.onDrag = false
}
lazy var sliderDragGesture: _EndedGesture<_ChangedGesture<DragGesture>> = DragGesture()
.onChanged { value in
self.onDrag = true
let dragLocation = value.location
//Restrict possible drag area
self.restrictSliderBtnLocation(dragLocation)
//Get current value
self.currentPercentage.wrappedValue = Double(self.currentLocation.x / self.sliderWidth)
}.onEnded { _ in
self.onDrag = false
}
private func restrictSliderBtnLocation(_ dragLocation: CGPoint) {
//On Slider Width
if dragLocation.x > CGPoint.zero.x && dragLocation.x < sliderWidth {
calcSliderBtnLocation(dragLocation)
}
}
private func calcSliderBtnLocation(_ dragLocation: CGPoint) {
if dragLocation.y != sliderHeight/2 {
currentLocation = CGPoint(x: dragLocation.x, y: sliderHeight/2)
} else {
currentLocation = dragLocation
}
}
//Current Value
var currentValue: Double {
return sliderValueStart + currentPercentage.wrappedValue * sliderValueRange
}
}
class CustomSlider: ObservableObject {
//Slider Size
let width: CGFloat = 300
let lineWidth: CGFloat = 8
//Slider value range from valueStart to valueEnd
let valueStart: Double
let valueEnd: Double
//Slider Handle
#Published var highHandle: SliderHandle
#Published var lowHandle: SliderHandle
//Handle start percentage (also for starting point)
#SliderValue var highHandleStartPercentage = 1.0
#SliderValue var lowHandleStartPercentage = 0.0
var anyCancellableHigh: AnyCancellable?
var anyCancellableLow: AnyCancellable?
init(start: Double, end: Double) {
valueStart = start
valueEnd = end
highHandle = SliderHandle(sliderWidth: width,
sliderHeight: lineWidth,
sliderValueStart: valueStart,
sliderValueEnd: valueEnd,
startPercentage: _highHandleStartPercentage
)
lowHandle = SliderHandle(sliderWidth: width,
sliderHeight: lineWidth,
sliderValueStart: valueStart,
sliderValueEnd: valueEnd,
startPercentage: _lowHandleStartPercentage
)
anyCancellableHigh = highHandle.objectWillChange.sink { _ in
self.objectWillChange.send()
}
anyCancellableLow = lowHandle.objectWillChange.sink { _ in
self.objectWillChange.send()
}
}
//Percentages between high and low handle
var percentagesBetween: String {
return String(format: "%.2f", highHandle.currentPercentage.wrappedValue - lowHandle.currentPercentage.wrappedValue)
}
//Value between high and low handle
var valueBetween: String {
return String(format: "%.2f", highHandle.currentValue - lowHandle.currentValue)
}
}
Slider implementation:
import SwiftUI
struct ContentView: View {
#ObservedObject var slider = CustomSlider(start: 10, end: 100)
var body: some View {
VStack {
Text("Value: " + slider.valueBetween)
Text("Percentages: " + slider.percentagesBetween)
Text("High Value: \(slider.highHandle.currentValue)")
Text("Low Value: \(slider.lowHandle.currentValue)")
//Slider
SliderView(slider: slider)
}
}
}
struct SliderView: View {
#ObservedObject var slider: CustomSlider
var body: some View {
RoundedRectangle(cornerRadius: slider.lineWidth)
.fill(Color.gray.opacity(0.2))
.frame(width: slider.width, height: slider.lineWidth)
.overlay(
ZStack {
//Path between both handles
SliderPathBetweenView(slider: slider)
//Low Handle
SliderHandleView(handle: slider.lowHandle)
.highPriorityGesture(slider.lowHandle.sliderDragGesture)
//High Handle
SliderHandleView(handle: slider.highHandle)
.highPriorityGesture(slider.highHandle.sliderDragGesture)
}
)
}
}
struct SliderHandleView: View {
#ObservedObject var handle: SliderHandle
var body: some View {
Circle()
.frame(width: handle.diameter, height: handle.diameter)
.foregroundColor(.white)
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 0)
.scaleEffect(handle.onDrag ? 1.3 : 1)
.contentShape(Rectangle())
.position(x: handle.currentLocation.x, y: handle.currentLocation.y)
}
}
struct SliderPathBetweenView: View {
#ObservedObject var slider: CustomSlider
var body: some View {
Path { path in
path.move(to: slider.lowHandle.currentLocation)
path.addLine(to: slider.highHandle.currentLocation)
}
.stroke(Color.green, lineWidth: slider.lineWidth)
}
}
This range slider will work with any width provided.
It uses GeometryReader to get the slider width
The slider is bounded by the value range and the thumbs handle cannot cross each other
RangeSliderView Usage
#State var sliderPosition: ClosedRange<Float> = 3...8
RangedSliderView(value: $sliderPosition, bounds: 1...10)
RangeSlideView Implementation
struct RangedSliderView: View {
let currentValue: Binding<ClosedRange<Float>>
let sliderBounds: ClosedRange<Int>
public init(value: Binding<ClosedRange<Float>>, bounds: ClosedRange<Int>) {
self.currentValue = value
self.sliderBounds = bounds
}
var body: some View {
GeometryReader { geomentry in
sliderView(sliderSize: geomentry.size)
}
}
#ViewBuilder private func sliderView(sliderSize: CGSize) -> some View {
let sliderViewYCenter = sliderSize.height / 2
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.nojaPrimary30)
.frame(height: 4)
ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)
// Calculate Left Thumb initial position
let leftThumbLocation: CGFloat = currentValue.wrappedValue.lowerBound == Float(sliderBounds.lowerBound)
? 0
: CGFloat(currentValue.wrappedValue.lowerBound - Float(sliderBounds.lowerBound)) * stepWidthInPixel
// Calculate right thumb initial position
let rightThumbLocation = CGFloat(currentValue.wrappedValue.upperBound) * stepWidthInPixel
// Path between both handles
lineBetweenThumbs(from: .init(x: leftThumbLocation, y: sliderViewYCenter), to: .init(x: rightThumbLocation, y: sliderViewYCenter))
// Left Thumb Handle
let leftThumbPoint = CGPoint(x: leftThumbLocation, y: sliderViewYCenter)
thumbView(position: leftThumbPoint, value: Float(currentValue.wrappedValue.lowerBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(max(0, dragLocation.x), sliderSize.width)
let newValue = Float(sliderBounds.lowerBound) + Float(xThumbOffset / stepWidthInPixel)
// Stop the range thumbs from colliding each other
if newValue < currentValue.wrappedValue.upperBound {
currentValue.wrappedValue = newValue...currentValue.wrappedValue.upperBound
}
})
// Right Thumb Handle
thumbView(position: CGPoint(x: rightThumbLocation, y: sliderViewYCenter), value: currentValue.wrappedValue.upperBound)
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(max(CGFloat(leftThumbLocation), dragLocation.x), sliderSize.width)
var newValue = Float(xThumbOffset / stepWidthInPixel) // convert back the value bound
newValue = min(newValue, Float(sliderBounds.upperBound))
// Stop the range thumbs from colliding each other
if newValue > currentValue.wrappedValue.lowerBound {
currentValue.wrappedValue = currentValue.wrappedValue.lowerBound...newValue
}
})
}
}
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.nojaPrimary, lineWidth: 4)
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(value))
.font(.secondaryFont(weight: .semibold, size: 10))
.offset(y: -20)
Circle()
.frame(width: 24, height: 24)
.foregroundColor(.nojaPrimary)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}
There are some improvements that can be added
e.g adding a step property or
also implementing the slider with a generic init to support Int, Float and other number types
use ViewBuilder to build a custom label for the slider
I modified the code from #culjo. This code supports preview and the logical parts are moved to viewModel.
import SwiftUI
struct RangeSlider: View {
#ObservedObject var viewModel: ViewModel
#State private var isActive: Bool = false
let sliderPositionChanged: (ClosedRange<Float>) -> Void
var body: some View {
GeometryReader { geometry in
sliderView(sliderSize: geometry.size,
sliderViewYCenter: geometry.size.height / 2)
}
.frame(height: ** insert your height of range slider **)
}
#ViewBuilder private func sliderView(sliderSize: CGSize, sliderViewYCenter: CGFloat) -> some View {
lineBetweenThumbs(from: viewModel.leftThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter),
to: viewModel.rightThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter))
thumbView(position: viewModel.leftThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter),
value: Float(viewModel.sliderPosition.lowerBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let newValue = viewModel.newThumbLocation(dragLocation: dragValue.location,
width: sliderSize.width)
if newValue < viewModel.sliderPosition.upperBound {
viewModel.sliderPosition = newValue...viewModel.sliderPosition.upperBound
sliderPositionChanged(viewModel.sliderPosition)
isActive = true
}
})
thumbView(position: viewModel.rightThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter),
value: Float(viewModel.sliderPosition.upperBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let newValue = viewModel.newThumbLocation(dragLocation: dragValue.location,
width: sliderSize.width)
if newValue > viewModel.sliderPosition.lowerBound {
viewModel.sliderPosition = viewModel.sliderPosition.lowerBound...newValue
sliderPositionChanged(viewModel.sliderPosition)
isActive = true
}
})
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(** insert your color **)
.frame(height: 4)
Path { path in
path.move(to: from)
path.addLine(to: to)
}
.stroke(isActive ? ** insert your color ** : ** insert your color **,
lineWidth: 4)
}
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
Circle()
.frame(size: .rangeSliderThumb)
.foregroundColor(isActive ? ** insert your color ** : ** insert your color **)
.contentShape(Rectangle())
.position(x: position.x, y: position.y)
.animation(.spring(), value: isActive)
}
}
extension RangeSlider {
final class ViewModel: ObservableObject {
#Published var sliderPosition: ClosedRange<Float>
let sliderBounds: ClosedRange<Int>
let sliderBoundDifference: Int
init(sliderPosition: ClosedRange<Float>,
sliderBounds: ClosedRange<Int>) {
self.sliderPosition = sliderPosition
self.sliderBounds = sliderBounds
self.sliderBoundDifference = sliderBounds.count - 1
}
func leftThumbLocation(width: CGFloat, sliderViewYCenter: CGFloat = 0) -> CGPoint {
let sliderLeftPosition = CGFloat(sliderPosition.lowerBound - Float(sliderBounds.lowerBound))
return .init(x: sliderLeftPosition * stepWidthInPixel(width: width),
y: sliderViewYCenter)
}
func rightThumbLocation(width: CGFloat, sliderViewYCenter: CGFloat = 0) -> CGPoint {
let sliderRightPosition = CGFloat(sliderPosition.upperBound - Float(sliderBounds.lowerBound))
return .init(x: sliderRightPosition * stepWidthInPixel(width: width),
y: sliderViewYCenter)
}
func newThumbLocation(dragLocation: CGPoint, width: CGFloat) -> Float {
let xThumbOffset = min(max(0, dragLocation.x), width)
return Float(sliderBounds.lowerBound) + Float(xThumbOffset / stepWidthInPixel(width: width))
}
private func stepWidthInPixel(width: CGFloat) -> CGFloat {
width / CGFloat(sliderBoundDifference)
}
}
}
struct RangeSlider_Previews: PreviewProvider {
static var previews: some View {
RangeSlider(viewModel: .init(sliderPosition: 2...8,
sliderBounds: 1...10),
sliderPositionChanged: { _ in })
}
}
When I'm trying to sign in and on the next view sign out with GIDSignIn and navigate to previous view everything is fine, but when I'm trying to sign in again, the alert ask App wants to use google sign in, when I press continue - I have an error says:
Keyboard cannot present view controllers (attempted to present )
and the next error
First responder error: non-key window attempting reload - allowing due to manual keyboard (first responder window is >, key window is ; layer = >)
My code
import SwiftUI
import Foundation
import GoogleSignIn
struct LoginView: View {
#ObservedObject var loginViewModel = LoginViewModel()
var body: some View {
NavigationView {
VStack {
Button(action: SocialLogin().attemptLoginGoogle, label: {
HStack{
Image("google")
Text("Google login")
.font(.title)
}
})
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.cornerRadius(8.0)
.shadow(radius: 4.0)
NavigationLink(destination: UserData(), isActive: self.$loginViewModel.isLogedIn) {
EmptyView()
}
}
.navigationBarTitle(Text("Login"))
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
struct SocialLogin: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<SocialLogin>) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<SocialLogin>) {
}
func attemptLoginGoogle() {
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
GIDSignIn.sharedInstance()?.signIn()
}
}
I encounter this problem in SwiftUI.
and I use this code.
if(GIDSignIn.sharedInstance()?.presentingViewController==nil){
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
}
GIDSignIn.sharedInstance()?.signIn()
change
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
to
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.first?.rootViewController
So the problem was in
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
You have to present in ViewController (in my case UIRepresentableViewController worked) otherwise it will tell you that keyboard can't show or present new View
Wrapper:
struct WrapedViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> LoginViewController {
let vc = LoginViewController()
print("\nmakeUIViewController \(vc)")
return vc
}
func updateUIViewController(_ uiViewController: LoginViewController, context: Context) {
print("updateUIViewController \(uiViewController)")
}
static func dismantleUIViewController(_ uiViewController: LoginViewController, coordinator: Self.Coordinator) {
print("dismantleUIViewController \(uiViewController)")
}
}
Wraped ViewController:
class LoginViewController: UIViewController {
override func viewDidLoad() {
let screenWidth = self.view.frame.size.width
let screenHeight = self.view.frame.size.height
let height: CGFloat = 40.0
let width: CGFloat = 120.0
let button = UIButton(frame: CGRect(x: (screenWidth / 2.0) - (width / 2.0),
y: (screenHeight / 2.0) - (height / 2.0),
width: width,
height: height))
button.backgroundColor = .green
button.setTitle("Test Button", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
self.view.addSubview(button)
}
#objc func buttonAction(sender: UIButton!) {
GIDSignIn.sharedInstance()?.presentingViewController = self
GIDSignIn.sharedInstance()?.signIn()
}
}