Until iOS 15.2 , the SwiftUI ScrollView doesn't support refreshable{}, so I using UIRefreshControl to solve this issues. Everything went smoothly and success was just one step away ;-(
full code ContentView.swift :
import SwiftUI
extension UIView {
func viewsInHierarchy<ViewType: UIView>() -> [ViewType]? {
var views: [ViewType] = []
viewsInHierarchy(views: &views)
return views.count > 0 ? views : nil
}
fileprivate func viewsInHierarchy<ViewType: UIView>(views: inout [ViewType]) {
subviews.forEach { eachSubView in
if let matchingView = eachSubView as? ViewType {
views.append(matchingView)
}
eachSubView.viewsInHierarchy(views: &views)
}
}
func searchViewAnchestorsFor<ViewType: UIView>(
_ onViewFound: (ViewType) -> Void
) {
if let matchingView = self.superview as? ViewType {
onViewFound(matchingView)
} else {
superview?.searchViewAnchestorsFor(onViewFound)
}
}
func searchViewAnchestorsFor<ViewType: UIView>() -> ViewType? {
if let matchingView = self.superview as? ViewType {
return matchingView
} else {
return superview?.searchViewAnchestorsFor()
}
}
func printViewHierarchyInformation(_ level: Int = 0) {
printViewInformation(level)
self.subviews.forEach { $0.printViewHierarchyInformation(level + 1) }
}
func printViewInformation(_ level: Int) {
let leadingWhitespace = String(repeating: " ", count: level)
let className = "\(Self.self)"
let superclassName = "\(self.superclass!)"
let frame = "\(self.frame)"
let id = (self.accessibilityIdentifier == nil) ? "" : " `\(self.accessibilityIdentifier!)`"
print("\(leadingWhitespace)\(className): \(superclassName)\(id) \(frame)")
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
//parent.navigationController?.navigationBar.prefersLargeTitles = true
//parent.navigationItem.largeTitleDisplayMode = .never
self.onResolve(parent)
// print("didMove(toParent: \(parent)")
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
HStack {
Spacer()
Text("Content")
Spacer()
}
Spacer()
}
.background(Color.red)
}
.navigationTitle("title")
.navigationBarTitleDisplayMode(.inline)
.overlay(
ViewControllerResolver { parent in
var scrollView: UIScrollView?
if let scrollViewsInHierarchy: [UIScrollView] = parent.view.viewsInHierarchy() {
if scrollViewsInHierarchy.count == 1, let firstScrollViewInHierarchy = scrollViewsInHierarchy.first {
scrollView = firstScrollViewInHierarchy
}
}
if let scrollView = scrollView {
guard scrollView.refreshControl == nil else { return }
let refreshControl = UIRefreshControl()
refreshControl.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
refreshControl.layoutIfNeeded()
let uiAction = UIAction(handler: { uiAction in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
refreshControl.endRefreshing()
}
})
refreshControl.addAction(uiAction, for: .primaryActionTriggered)
scrollView.refreshControl = refreshControl
}
}
.frame(width: 0, height: 0)
)
}
}
}
Related
I'm new to swiftui and swift basically, i made One-time-code screen, and here i have a problem. When i run project on my old phone (iphone 6) when keyboard appears, my textfield size changes (it gets very thin vertically). So i was wondering is there any way to add autolayout for older devices?
Here is my code
struct OneTimeCodeBoxes: View {
#Binding var codeDict: [Int: String]
#State var firstResponderIndex = 0
var onCommit: (()->Void)?
var body: some View {
HStack {
ForEach(0..<codeDict.count) { i in
let isEmpty = codeDict[i]?.isEmpty == true
OneTimeCodeInput(
index: i,
codeDict: $codeDict,
firstResponderIndex: $firstResponderIndex,
onCommit: onCommit
)
.aspectRatio(1, contentMode: .fit)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: isEmpty ? 1 : 2)
.foregroundColor(isEmpty ? .secondary : .green))
}
}
}
}
struct OneTimeCodeBoxes_Previews: PreviewProvider {
static var previews: some View {
OneTimeCodeBoxes(codeDict: .constant([0: "", 1: "", 2: "", 3: ""]))
.padding()
.previewLayout(.sizeThatFits)
}
}
Here is my OneTimeCodeInput part of code
struct OneTimeCodeInput: UIViewRepresentable {
typealias UIViewType = UITextField
let index: Int
#Binding var codeDict: [Int: String]
#Binding var firstResponderIndex: Int
var onCommit: (()->Void)?
class Coordinator: NSObject, UITextFieldDelegate {
let index: Int
#Binding var codeDict: [Int: String]
#Binding var firstResponderIndex: Int
private lazy var codeDigits: Int = codeDict.count
init(index: Int, codeDict: Binding<[Int: String]>, firstResponderIndex: Binding<Int>) {
self.index = index
self._codeDict = codeDict
self._firstResponderIndex = firstResponderIndex
}
#objc func textFieldEditingChanged(_ textField: UITextField) {
print("textField.text!", textField.text!)
guard textField.text!.count == codeDigits else { return }
codeDict = textField.text!.enumerated().reduce(into: [Int: String](), { dict, tuple in
let (index, char) = tuple
dict.updateValue(String(char), forKey: index)
})
firstResponderIndex = codeDigits - 1
}
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
{
print("replacementString", string)
if string.isBackspace {
codeDict.updateValue("", forKey: index)
firstResponderIndex = max(0, textField.text == "" ? index - 1 : index)
return false
}
for i in index..<min(codeDict.count, index + string.count) {
codeDict.updateValue(string.stringAt(index: i - index), forKey: i)
// print(codeDict)
firstResponderIndex = min(codeDict.count - 1, index + string.count)
}
return true
}
}
func makeCoordinator() -> Coordinator {
.init(index: index, codeDict: $codeDict, firstResponderIndex: $firstResponderIndex)
}
func makeUIView(context: Context) -> UITextField {
let tf = BackspaceTextField(onDelete: {
firstResponderIndex = max(0, index - 1)
})
tf.addTarget(context.coordinator, action: #selector(Coordinator.textFieldEditingChanged), for: .editingChanged)
tf.delegate = context.coordinator
tf.keyboardType = .numberPad
tf.textContentType = .oneTimeCode
tf.font = .preferredFont(forTextStyle: .largeTitle)
tf.textAlignment = .center
return tf
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = codeDict[index]
if index == firstResponderIndex {
uiView.becomeFirstResponder()
}
if index == firstResponderIndex,
codeDict.values.filter({ !$0.isEmpty }).count == codeDict.count
{
onCommit?()
}
}
}
extension OneTimeCodeInput {
class BackspaceTextField: UITextField {
var onDelete: (()->Void)?
init(onDelete: (()->Void)?) {
self.onDelete = onDelete
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func deleteBackward() {
super.deleteBackward()
onDelete?()
}
}
}
This is how it shows on Ipnone13 simulator, and it's correct. I'm trying to do same on older devices
I have a situation where 9 buttons are placed on viewController and making tic-tac-toe function on ios. whenever I am trying to reset button images once the game is finished, button images are not getting reset.
class TicTacToeViewController: UIViewController {
#IBOutlet weak var gameBoardView: UIView!
#IBOutlet var buttons: [UIButton]!
var viewModel = TicTacToeViewModel()
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
func configureUI() {
viewModel.delegate = self
}
#IBAction func buttonClicked(_ sender: UIButton) {
viewModel.processPlayerMove(for: sender.tag)
}
/// This **function is to reset the view means remove close and circle image once player wants to restart the game. But this code is not working**
func updateButtonView() {
DispatchQueue.main.async {
for (index, moves) in self.viewModel.moves.enumerated() {
let button = self.buttons[index]
if let moves = moves {
let image = UIImage(named: moves.indicator)
button.setImage(image, for: .normal)
} else {
button.setImage(nil, for: .normal)
}
}
}
}
}
/// Delegates are called from viewmodel
extension TicTacToeViewController: TicTacToeViewModelProtocol {
func updatePlayerInfo(index: Int) {
let systemImageName = viewModel.moves[index - 1]?.indicator ?? ""
let image = UIImage(named: systemImageName)
if let arrayButtons = buttons {
let button = arrayButtons[index - 1]
button.setImage(image, for: .normal)
}
}
func displayAlert(alertData: AlertItem) {
let alert = UIAlertController(title: alertData.title, message: alertData.message, preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: alertData.buttonTitle, style: UIAlertAction.Style.default, handler: { [weak self] _ in
self?.viewModel.resetGame()
self?.updateButtonView()
}))
self.present(alert, animated: true, completion: nil)
}
func gameBoardDisabled(isGameBoardDisable: Bool) {
gameBoardView.isUserInteractionEnabled = !isGameBoardDisable
}
}
Please let me know of any confusion. Thanks in advance for your help.
This is view model class
import Foundation
protocol TicTacToeViewModelProtocol: AnyObject {
func updatePlayerInfo(index: Int)
func displayAlert(alertData: AlertItem)
func gameBoardDisabled(isGameBoardDisable: Bool)
}
enum Player {
case human
case computer
}
class TicTacToeViewModel {
var currentMovePosition: Int?
var moves: [Move?] = Array(repeating: nil, count: 9) {
didSet {
if let delegate = delegate, let position = self.currentMovePosition {
delegate.updatePlayerInfo(index: position)
}
}
}
var isGameBoardDisable = false {
didSet {
if let delegate = delegate {
delegate.gameBoardDisabled(isGameBoardDisable: isGameBoardDisable)
}
}
}
var alertItem: AlertItem? {
didSet {
if let delegate = delegate, let alertItem = alertItem {
delegate.displayAlert(alertData: alertItem)
}
}
}
let winPatterns: Set<Set<Int>> = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 4, 7], [2, 5, 8], [3, 6, 9], [1, 5, 9], [3, 5, 7]]
weak var delegate: TicTacToeViewModelProtocol!
func processPlayerMove(for position: Int) {
self.currentMovePosition = position
if isSquareOccupied(in: moves, forIndex: position) { return }
moves[position - 1] = Move(player: .human, boardIndex: position)
// logic for win, draw or lose
if checkWinCondition(for: .human, in: moves) {
alertItem = AlertContext.humanWin
return
}
if checkForDraw(in: moves) {
alertItem = AlertContext.draw
return
}
isGameBoardDisable = true
processComputerMove()
}
func processComputerMove() {
let computerPosition = determinComputerMovePosition(in: moves)
self.currentMovePosition = computerPosition
moves[computerPosition - 1] = Move(player: .computer, boardIndex: computerPosition)
isGameBoardDisable = false
if checkWinCondition(for: .computer, in: moves) {
alertItem = AlertContext.computerWin
return
}
if checkForDraw(in: moves) {
alertItem = AlertContext.draw
return
}
}
func isSquareOccupied(in moves:[Move?], forIndex index: Int) -> Bool {
return moves.contains(where: { $0?.boardIndex == index })
}
func determinComputerMovePosition(in moves: [Move?]) -> Int {
let computerMoves = moves.compactMap { $0 }.filter { $0.player == .computer }
let computerPositions = Set(computerMoves.map { $0.boardIndex })
for pattern in winPatterns {
let winPositions = pattern.subtracting(computerPositions)
if winPositions.count == 1 {
let isAvailable = !isSquareOccupied(in: moves, forIndex: winPositions.first!)
if isAvailable { return winPositions.first! }
}
}
// if the AI can't finish the game it will block
let humanMoves = moves.compactMap { $0 }.filter { $0.player == .human }
let humanPositions = Set(humanMoves.map { $0.boardIndex })
for pattern in winPatterns {
let winPositions = pattern.subtracting(humanPositions)
if winPositions.count == 1 {
let isAvailable = !isSquareOccupied(in: moves, forIndex: winPositions.first!)
if isAvailable { return winPositions.first! }
}
}
// always take the middle block
let middleSquare = 5
if !isSquareOccupied(in: moves, forIndex: middleSquare) {
return middleSquare
}
// if the AI can't get position middle block it will get a random position
var movePosition = Int.random(in: 1..<10)
while isSquareOccupied(in: moves, forIndex: movePosition) {
movePosition = Int.random(in: 1..<10)
}
return movePosition
}
func checkWinCondition(for player: Player, in moves:[Move?]) -> Bool {
let playerMoves = moves.compactMap({ $0 }).filter { $0.player == player }
let playerPositions = Set(playerMoves.map { $0.boardIndex })
for pattern in winPatterns where pattern.isSubset(of: playerPositions) {return true}
return false
}
func checkForDraw(in moves: [Move?]) -> Bool {
return moves.compactMap { $0 }.count == 9
}
func resetGame() {
moves = Array(repeating: nil, count: 9)
}
}
I can't figure out how to make this apple sign-in button wider and taller. no matter where I try to add .frame(width: .... it just seems to move the button around the screen. But, does not alter the dimensions of the button itself.
This is in ContentView.swift:
struct ContentView : View {
#State var credentials: CredentialsOrError?
var body: some View {
VStack {
if $credentials.wrappedValue == nil {
SignInWithAppleButton(credentials: $credentials)
}
else if $credentials.wrappedValue!.isSuccess
{
Text("User: \($credentials.wrappedValue!.values!.user)")
Text("Given name: \($credentials.wrappedValue!.values?.givenName ?? "")")
Text("Family name: \($credentials.wrappedValue!.values?.familyName ?? "")")
Text("Email: \($credentials.wrappedValue!.values?.email ?? "")")
}
else {
Text($credentials.wrappedValue!.error!.localizedDescription).foregroundColor(.red)
}
}.fullScreenCover(isPresented: .constant($credentials.wrappedValue != nil && $credentials.wrappedValue!.isSuccess) , content: {
HomeView()
})
}
}
This is from SignInWithAppleButton.swift:
struct SignInWithAppleButton: View {
#Binding var credentials: CredentialsOrError?
var body: some View {
let button = ButtonController(credentials: $credentials)
return button
.frame(width: button.button.frame.width, height: button.button.frame.height, alignment: .center)
}
struct ButtonController: UIViewControllerRepresentable {
let button: ASAuthorizationAppleIDButton = ASAuthorizationAppleIDButton()
let vc: UIViewController = UIViewController()
#Binding var credentials: CredentialsOrError?
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: Context) -> UIViewController {
vc.view.addSubview(button)
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
class Coordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
let parent: ButtonController
init(_ parent: ButtonController) {
self.parent = parent
super.init()
parent.button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
}
#objc func didTapButton() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.presentationContextProvider = self
authorizationController.delegate = self
authorizationController.performRequests()
}
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return parent.vc.view.window!
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential else {
parent.credentials = .error("Credentials are not of type ASAuthorizationAppleIDCredential")
return
}
parent.credentials = .credentials(user: credentials.user, givenName: credentials.fullName?.givenName, familyName: credentials.fullName?.familyName, email: credentials.email)
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
parent.credentials = .error(error)
}
}
}
}
Attached is a picture of what it looks like currently on my dark mode iPhone and I've also attached a picture of the light mode emulator.
Related question: How can I hard code the background of the screen to be white?
In the emulator:
On my dark mode iPhone:
I am trying to build an app which loops through overlay tiles. The problem is the map tiles take forever to reload when the map displays. What is the best way around this issue? I'm not sure if it is a caching issue which I think MapKit does itself. My guess is it is a Swift redrawing issue. My code is below, and I appreciate your help.
// Copyright 2020 Oklahoma Weather Blog
//
import SwiftUI
import MapKit
import SwiftSoup
/*
struct RadarMapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
var tileRenderer = MKOverlayRenderer()
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
guard let tileOverlay = overlay as? MKTileOverlay else {
return MKOverlayRenderer()
}
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
}
func makeUIView(context: Context) -> MKMapView {
var template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
template = "https://tilecache.rainviewer.com/v2/ radar/1600575600/512/{z}/{x}/{y}/6/0_1.png"
let overlay = MKTileOverlay(urlTemplate:template)
overlay.canReplaceMapContent = false
let mapView = MKMapView(frame: .zero)
var renderedOverlay = MKTileOverlayRenderer(tileOverlay: overlay)
mapView.addOverlay(overlay, level: .aboveLabels)
mapView.setNeedsDisplay()
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.3, longitudeDelta: 0.3)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
}
*/
func getTimesURL() -> [String] {
let myURLString = "https://api.rainviewer.com/public/maps.json"
guard let myURL = URL(string: myURLString) else {
printToConsole("Error: \(myURLString) doesn't seem to be a valid URL")
return []
}
do {
let myHTMLString = try String(contentsOf: myURL)
do {
let doc: Document = try SwiftSoup.parse(myHTMLString)
let text = try doc.text()
let resultArray = text.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
.components(separatedBy:",")
return resultArray
} catch Exception.Error( _, let message) {
printToConsole(message)
} catch {
printToConsole("error")
}
} catch let error as NSError {
printToConsole("Error: \(error)")
}
return []
}
func getOverlays() -> [MKTileOverlay] {
var overlays: [MKTileOverlay] = []
for time in getTimesURL() {
let template = "https://tilecache.rainviewer.com/v2/radar/\(time)/256/{z}/{x}/{y}/7/1_1.png"
let overlay = MKTileOverlay(urlTemplate:template)
overlays.append(overlay)
}
return overlays
}
struct RadarView: View {
private static let mapStyles: [MKMapType] = [.hybrid, .hybridFlyover, .mutedStandard, .satellite, .satelliteFlyover, .standard]
#State var mapTime = "1600585200"
let cache = NSCache<NSString, MKTileOverlay>()
#AppStorage("selectedMapStyle") var selectedMapStyle = 0
#State private var showingSheet = false
private static var overlayArray: [MKTileOverlay] {
getOverlays()
}
private static var timeArray: [String] {
getTimesURL()
}
func dateToString(_ epoch: String) -> String{
let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "MM/dd hh:mm a"
let date = Date(timeIntervalSince1970: TimeInterval(Int(epoch)!))
return dateFormatterPrint.string(from: date)
}
#State private var timeIndex: Double = 0
#State private var loopMap: Bool = false
#State var radarTimer: Timer?
var body: some View {
VStack{
ZStack(alignment: .top) {
RadarMapView(mapStyle: RadarView.mapStyles[selectedMapStyle], overlay: RadarView.overlayArray[Int(timeIndex)]).edgesIgnoringSafeArea(.bottom)
HStack{
VStack{
Slider(value: $timeIndex.animation(.linear), in: 0...9, step: 1)
Text("\(dateToString(RadarView.timeArray[Int(timeIndex)]))")
}
Button(action: {
loopMap.toggle()
if loopMap {
radarTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
// do something here
if RadarView.overlayArray.count > 0 {
withAnimation{
timeIndex = Double((Int(timeIndex) + 1) % RadarView.overlayArray.count )
}
}
}
} else {
radarTimer?.invalidate()
}
}, label: {
if !loopMap { Text("Loop") }
else { Text("Pause") }
})
}.padding(.horizontal).padding(.horizontal).background(Color.init(UIColor.systemBackground).opacity(0.75))
}
HStack(){
Spacer()
Button(action: {
/*
selectedMapStyle = (selectedMapStyle + 1) % mapStyles.count
*/
showingSheet.toggle()
}, label: {
Image(systemName: "map.fill").resizable()
.renderingMode(.template)
.font(.title)
.foregroundColor(Color.primary)
.frame(width: 20, height: 20)
}).padding()
}.padding(.horizontal).padding(.bottom).background(Color.init(UIColor.systemBackground).opacity(0.75))
}.actionSheet(isPresented: $showingSheet) {
ActionSheet(title: Text("What map style do you want?"), message: Text("Please select one option below"), buttons: [
.default(Text("Muted")) { self.selectedMapStyle = 2 },
.default(Text("Satellite")) { self.selectedMapStyle = 3 },
.default(Text("Satellite w/ Roads")) { self.selectedMapStyle = 0 },
.default(Text("Satellite 3-D")) { self.selectedMapStyle = 4 },
.default(Text("3-D Satellite w/ Roads")) { self.selectedMapStyle = 1 },
.default(Text("Standard")) { self.selectedMapStyle = 5 },
.cancel(Text("Dismiss"))
])
}.edgesIgnoringSafeArea(.bottom).navigationBarTitle("Radar")
}
}
struct RadarMapView: UIViewRepresentable {
var mapStyle: MKMapType
var overlay: MKTileOverlay
class Coordinator: NSObject, MKMapViewDelegate {
var parent: RadarMapView
init(_ parent: RadarMapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKTileOverlayRenderer(overlay: overlay)
return renderer
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ mapView: MKMapView, context: Context) {
// var template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
//1600626600
mapView.mapType = self.mapStyle
mapView.delegate = context.coordinator
let overlays = mapView.overlays
mapView.addOverlay(overlay)
let regionRadius: CLLocationDistance = 50000
let location = CLLocationCoordinate2D(latitude: 33.7490, longitude: -84.3880)
let coordinateRegion = MKCoordinateRegion(center: location,
latitudinalMeters: regionRadius * 2.0, longitudinalMeters: regionRadius * 2.0)
mapView.setRegion(coordinateRegion, animated: true)
for overlay in overlays {
// remove all MKPolyline-Overlays
if overlay is MKTileOverlay {
mapView.removeOverlay(overlay)
}
}
}
}
struct RadarView_Previews: PreviewProvider {
static var previews: some View {
RadarView()
}
}
I had a similar problem a few days ago (with a different tile server) and solved it by using 512px tiles.
My theory is, that almost nobody uses 256px tiles, so servers don't cache them.
Do something like
func getOverlays() -> [MKTileOverlay] {
var overlays: [MKTileOverlay] = []
for time in getTimesURL() {
let template = "https://tilecache.rainviewer.com/v2/radar/\(time)/512/{z}/{x}/{y}/7/1_1.png"
let overlay = MKTileOverlay(urlTemplate:template)
overlay.tileSize = CGSize(width: 512, height: 512)
overlays.append(overlay)
}
return overlays
}
I looked and cannot find an answer that works for me. I have subclassed UIControl to create a double-knob slider control. I want each knob to be available for voiceover.
To do this, I create UIAccessibilityElements and add them to an array:
func addAccessibilityElements() {
axKnobs = []
let lowKnob = UIAccessibilityElement(accessibilityContainer: self)
lowKnob.accessibilityLabel = doubleKnob ? lowValueKnobAccessibilityLabel : valueKnobAccessibilityLabel
lowKnob.accessibilityPath = UIAccessibilityConvertPathToScreenCoordinates(knobBezierPath(lowKnobPoint), self)
lowKnob.accessibilityTraits = UIAccessibilityTraitAdjustable
lowKnob.accessibilityValue = "\(lowValue)"
axKnobs.append(lowKnob)
if doubleKnob, let highKnobPoint = highKnobPoint {
let highKnob = UIAccessibilityElement(accessibilityContainer: self)
highKnob.accessibilityLabel = highValueKnobAccessibilityLabel
highKnob.accessibilityPath = UIAccessibilityConvertPathToScreenCoordinates(knobBezierPath(highKnobPoint), self)
highKnob.accessibilityTraits = UIAccessibilityTraitAdjustable
highKnob.accessibilityValue = "\(highValue)"
axKnobs.append(highKnob)
}
}
This seems to work perfect. These methods are called and the interface seems to work right:
override func accessibilityElementCount() -> Int {
return axKnobs.count
}
override func indexOfAccessibilityElement(element: AnyObject) -> Int {
let index = axKnobs.indexOf(element as! UIAccessibilityElement)!
if index == 0 {
currentKnob = .Low
} else {
currentKnob = .High
}
return index
}
override func accessibilityElementAtIndex(index: Int) -> AnyObject? {
return axKnobs[index]
}
However, my last 2 methods (accessibilityIncrement and accessibilityDecrement) in the class extension aren't being called at all.
override func accessibilityIncrement() {
if currentKnob == .None {
return
}
if currentKnob == .High {
highValue = max(highValue + 10, maximumValue)
} else {
if doubleKnob {
lowValue = max(lowValue + 10, highValue - 1)
} else {
lowValue = max(lowValue + 10, maximumValue)
}
}
updateDelegate()
redraw()
}
override func accessibilityDecrement() {
if currentKnob == .None {
return
}
if currentKnob == .High {
highValue = min(highValue - 10, lowValue + 1)
} else {
lowValue = min(lowValue - 10, minimumValue)
}
updateDelegate()
redraw()
}
Any ideas why? Full project available at https://github.com/AaronBratcher/SliderTest
UIAccessibilityElements have those 2 methods called, not the UIControl subclass.
extension DLSlider {
class KnobAccessibilityElement: UIAccessibilityElement {
var onIncrement: ((knob: UIAccessibilityElement) -> Void)?
var onDecrement: ((knob: UIAccessibilityElement) -> Void)?
override func accessibilityIncrement() {
if let callback = onIncrement {
callback(knob: self)
}
}
override func accessibilityDecrement() {
if let callback = onDecrement {
callback(knob: self)
}
}
}
func addAccessibilityElements() {
axKnobs = []
let lowKnob = KnobAccessibilityElement(accessibilityContainer: self)
lowKnob.accessibilityLabel = doubleKnob ? lowValueKnobAccessibilityLabel : valueKnobAccessibilityLabel
lowKnob.accessibilityPath = UIAccessibilityConvertPathToScreenCoordinates(knobBezierPath(lowKnobPoint), self)
lowKnob.accessibilityTraits = UIAccessibilityTraitAdjustable
lowKnob.accessibilityValue = "\(lowValue)"
lowKnob.onIncrement = { [unowned self] (knob: UIAccessibilityElement) in
self.incrementKnob(knob)
}
lowKnob.onDecrement = { [unowned self] (knob: UIAccessibilityElement) in
self.decrementKnob(knob)
}
axKnobs.append(lowKnob)
if doubleKnob, let highKnobPoint = highKnobPoint {
let highKnob = KnobAccessibilityElement(accessibilityContainer: self)
highKnob.accessibilityLabel = highValueKnobAccessibilityLabel
highKnob.accessibilityPath = UIAccessibilityConvertPathToScreenCoordinates(knobBezierPath(highKnobPoint), self)
highKnob.accessibilityTraits = UIAccessibilityTraitAdjustable
highKnob.accessibilityValue = "\(highValue)"
highKnob.onIncrement = { [unowned self] (knob: UIAccessibilityElement)in
self.incrementKnob(knob)
}
highKnob.onDecrement = { [unowned self] (knob: UIAccessibilityElement) in
self.decrementKnob(knob)
}
axKnobs.append(highKnob)
}
}
override func accessibilityElementCount() -> Int {
return axKnobs.count
}
override func indexOfAccessibilityElement(element: AnyObject) -> Int {
return axKnobs.indexOf(element as! UIAccessibilityElement)!
}
override func accessibilityElementAtIndex(index: Int) -> AnyObject? {
return axKnobs[index]
}
... // other methods here
}