Should I call viewDidLoad() inside updateUIViewController(_:context:) in SwiftUI - uiviewcontroller

I create UIScrollView to be integrated inside SwiftUI view. It contains UIHostingController to host SwiftUI view. When I update UIHostingController, UIScrollView does not change its constraints. I can scroll neither to top nor to bottom. When I try to call viewDidLoad() inside updateUIViewController(_:context:), it works like I expect. Here is my sample code,
struct ContentView: View {
#State private var max = 100
var body: some View {
VStack {
Button("Add") { self.max += 2 }
ScrollableView {
ForEach(0..<self.max, id: \.self) { index in
Text("Hello \(index)")
.frame(width: UIScreen.main.bounds.width, height: 100)
.background(Color(red: Double.random(in: 0...255) / 255, green: Double.random(in: 0...255) / 255, blue: Double.random(in: 0...255) / 255))
}
}
}
}
}
class ScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView: UIScrollView = UIScrollView()
override func viewDidLoad() {
self.view = UIView()
self.addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(hostingController.view)
scrollView.delegate = self
scrollView.scrollsToTop = true
scrollView.isScrollEnabled = true
makeConstraints()
hostingController.didMove(toParent: self)
}
func makeConstraints() {
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
}
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
let vc = ScrollViewController(rootView: self.content())
return vc
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = self.content()
viewController.viewDidLoad()
}
}
I don't think it is a good way to do. I want to know if there is the best way to update controller. If anyone knows the best solution, share me please. Thanks.

You are correct, we should never call our own viewDidLoad.
Let’s diagnose the issue, using the view debugger. So, for example, here it is (setting max to 8 to keep it manageable):
Note the height of the hosting controller’s view is 800 (because we have 8 subviews, 100 pt each). So far, so good.
Now tap the “add” button and repeat:
We can see that the problem isn’t the scroll view, but rather the hosting view controller’s view. Even though there are now 10 items, it still thinks the hosting view controller’s view’s height is 800.
So, we can call setNeedsUpdateConstraints and that fixes the problem:
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = content()
viewController.hostingController.view.setNeedsUpdateConstraints()
}
Thus:
struct ContentView: View {
#State private var max = 8
var body: some View {
GeometryReader { geometry in // don't reference `UIScreen.main.bounds` as that doesn’t work in split screen multitasking
VStack {
Button("Add") { self.max += 2 }
ScrollableView {
ForEach(0..<self.max, id: \.self) { index in
Text("Hello \(index)")
.frame(width: geometry.size.width, height: 100)
.background(Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)))
}
}
}
}
}
}
class ScrollViewController<Content: View>: UIViewController {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad() // you need to call `super`
// self.view = UIView() // don't set `self.view`
addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(hostingController.view)
// scrollView.delegate = self // you're not currently using this delegate protocol, so we probably shouldn't set the delegate
// scrollView.scrollsToTop = true // these are the default values
// scrollView.isScrollEnabled = true
makeConstraints()
hostingController.didMove(toParent: self)
}
func makeConstraints() {
NSLayoutConstraint.activate([
// constraints for scroll view w/in main view
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// define contentSize of scroll view relative to hosting controller's view
hostingController.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor)
])
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
}
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
ScrollViewController(rootView: content())
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = content()
viewController.hostingController.view.setNeedsUpdateConstraints()
}
}

Related

Passing data from StructA to VCA, and then from VCA to VCB

i hope im not breaking any rules, I've got a problem when im trying to pass data from struct to A and from A to B, now the problem happens when im trying to pass the data from B to C. it all works fine when i use delegates from A to B.
before i post my code, i would mention few things:
I parse JSON and use delegate to pass the data from A(is my struct) to B
I would like to send data from VCAA to VCB.
Here's my code:
my struct:
import Foundation
import UIKit
// MARK: - Struct Protocol
protocol QuizBrainDelegate {
func didUpdateQuestionsArray(questionsArr: [Questions])
func didUpdateMessage(message: String)
}
// MARK: -
struct QuizBrain {
let urlString = "https://5fa952f1c9b4e90016e6a5be.mockapi.io/data"
var delegate: QuizBrainDelegate?
// MARK: API Request.
func performRequest() {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, respone, error) in
if error != nil {
print("There's an error \(error!)")
}
if let safeData = data {
self.parseJSON(with: safeData)
}
}
task.resume()
}
}
/// parsing JSON method to parse the JSON
/// - Parameter data: The data returned by the server
func parseJSON(with data: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(Root.self, from: data)
let questionsArr = decodedData.data.questions // an array of questions
let thankUMessage = decodedData.data.thank_you_message
//passing the quiz arr to our Quiz's VC:
self.delegate?.didUpdateQuestionsArray(questionsArr: questionsArr)
self.delegate?.didUpdateMessage(message: thankUMessage)
} catch {
print("There was a problem with parsing JSON \(error)")
}
}
// MARK: - Struct Methods:
/// This func gets the next question everytime we answer the question
/// - Parameters:
/// - questionNum: a counter of the current question number.
/// - numOfQuestions: a counter of total amount of questions.
/// - Returns: returns the new value of questionNum which is the counter of our question number.
func nextQuestion(questionNum: Int, numOfQuestions: Int) -> Int {
return questionNum + 1
}
/// Func sets the score of the player
/// - Parameter scoreNum: the total score number.
/// - Returns: returns the score with 5 points once the user answers right
func getNumOfCurretQuestion(scoreNum: Int) ->Int {
return scoreNum+1
}
}
my VCA:
import UIKit
import Foundation
protocol QuizVCDelegate {
func changeTitle(_ message: String?)
func updateUserOptions(_ optionsArr: [String])
}
class QuizViewController: UIViewController, QuizBrainDelegate {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var answerOption1: UIButton!
#IBOutlet weak var answerOption2: UIButton!
#IBOutlet weak var answerOption3: UIButton!
#IBOutlet weak var answerOption4: UIButton!
#IBOutlet weak var currQuestionLabel: UILabel!
var currentQuestionCounter = 0 // user's current number.
var numOfQuestion = 0 // counter of total questions.
var numOfOptions = 0 // counter of total options for each question
var quizBrain = QuizBrain() // an instance of struct QuizBrain for following MVC.
var messageToDisplay = ""
var quizArr = [Questions]() // array of Q and A
var storedAnswers = [String]() // an array of stored answers of the user
var delegate: QuizVCDelegate?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
quizBrain.performRequest() // Calling the URLRequest.
}
override func viewDidLoad() {
super.viewDidLoad()
quizBrain.delegate = self
}
#IBAction func answerButtonPressed(_ sender: UIButton) {
guard let userAnswer = sender.currentTitle else { return }
storedAnswers.append(userAnswer) // Storing the User's answers.
delegate?.updateUserOptions(storedAnswers)
delegate?.changeTitle(self.messageToDisplay)
sender.pulsate() // lets the user knows that he answered the question.
numOfQuestion = quizBrain.nextQuestion(questionNum: numOfQuestion, numOfQuestions: quizArr.count)
if numOfQuestion == quizArr.count { // checking if its equal to the total of questions in the array.
switchScreen()
}
//to make smooth transitaions im using a timer to update the UI:
if numOfQuestion < quizArr.count {
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(updateUI), userInfo: nil, repeats: false)
}
}
// a func that updates all the UI
#objc func updateUI() {
questionLabel.fadeTransition(0.4) // fade animation to our questionLabel
self.questionLabel.text = quizArr[numOfQuestion].question
while numOfOptions < quizArr[numOfQuestion].options.count {
switch numOfOptions { // 2-4 Options.
case 0: // none
self.answerOption1.setTitle(self.quizArr[numOfQuestion].options[numOfOptions].value, for: .normal) // Updates option1
case 1: // Joe
self.answerOption2.setTitle(self.quizArr[numOfQuestion].options[numOfOptions].value, for: .normal) // Updates option2
case 2: // Trump
self.answerOption3.setTitle(self.quizArr[numOfQuestion].options[numOfOptions].value, for: .normal) // Updates option3
case 3:
print("There's a case 4")
default:
print("There's a problem with Options Switch Statement")
}
numOfOptions+=1
}
numOfOptions = 0
currQuestionLabel.fadeTransition(0.4) // fade animation to our currentQuestion
currQuestionLabel.text = "Total: \(currentQuestionCounter)/\(numOfQuestion)" // updates the score.
}
/// func to update the arr with the JSON decoded questions and answers.
/// - Parameter questionsArr: an array of question objects.
func didUpdateQuestionsArray(questionsArr: [Questions]) {
DispatchQueue.main.async {
self.quizArr = questionsArr
self.updateUI()
}
}
func didUpdateMessage(message: String) {
DispatchQueue.main.async {
self.messageToDisplay = message
print(self.messageToDisplay)
}
}
// a func which presents our Thank you VC.
func switchScreen() {
let mainStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
if let viewController = mainStoryboard.instantiateViewController(withIdentifier: "sbThanks") as? UIViewController {
viewController.modalPresentationStyle = .fullScreen
viewController.modalTransitionStyle = .crossDissolve
self.present(viewController, animated: true, completion: nil)
}
}
}
my VCB:
import Foundation
import UIKit
class ThanksViewController: UIViewController, QuizVCDelegate {
func updateUserOptions(_ optionsArr: [String]) {
DispatchQueue.main.async {
self.choosenAnswers = optionsArr
}
}
func changeTitle(_ message: String?) {
DispatchQueue.main.async {
self.titleLabel.text = message
}
}
var titleLabel = UILabel()
let bodyLabel = UILabel()
var choosenAnswers = [String]()
var quizVC = QuizViewController()
fileprivate func setupLabels() {
titleLabel.lineBreakMode = .byClipping // avoiding the 3 dots.
titleLabel.font = UIFont(name: "Futura", size: 20)
titleLabel.textColor = UIColor.black
titleLabel.textAlignment = .center
bodyLabel.text = "Your Answers:\n\(choosenAnswers)"
bodyLabel.numberOfLines = 0
bodyLabel.textColor = UIColor.black
bodyLabel.textAlignment = .center
}
fileprivate func setupStackView() {
let stackView = UIStackView(arrangedSubviews: [titleLabel, bodyLabel])
stackView.axis = .vertical
stackView.spacing = 8
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -100).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
quizVC.delegate = self
setupLabels()
setupStackView()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapAnimations)))
}
#objc fileprivate func handleTapAnimations() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseOut) {
self.titleLabel.transform = CGAffineTransform(translationX: -30, y: 0)
} completion: { (_) in
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.titleLabel.alpha = 0
self.titleLabel.transform = self.titleLabel.transform.translatedBy(x: 0, y: -100)
})
}
UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseOut) {
self.bodyLabel.transform = CGAffineTransform(translationX: -30, y: 0)
} completion: { (_) in
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.bodyLabel.alpha = 0
self.bodyLabel.transform = self.bodyLabel.transform.translatedBy(x: 0, y: -100)
})
}
}
}

Load html text in WKWebView

I use this code to load my html file with text in WKWebView:
do {
guard let filePath = Bundle.main.path(forResource: "\(readBookNumber)", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: UserDefaults.standard.integer(forKey: "textSize"), fontColor: textColor)
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
}
catch {
print ("File HTML error")
}
and this code to load the page where the user stopped reading last time:
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
I use code for loading last page in this method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
At first I used deadline: .now() + 0.1, but that didn't work. Because the last read page was loaded initially, and after a few seconds I see my text on the first page. I change it to deadline: .now() + 0.5 and the text loads fine from the last page read. Its was 700 pages. But now I want to load another text with 1700 pages. And I have same problem like first time. I can change deadline: .now() + 1.0 and my text will load fine. But I think this is not the best solution. I run it on my iPhone X. But maybe if I run it on iPad mini 2 I should change deadline: .now() + 10.0 because iPad mini 2 not very powerful. How to solve the problem?
Update based on #DPrice code:
If I use this code:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
I have same bad result like in my code.
But if I use this code:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
}
Everything works fine. And my last page loading fine. But it does not solve the problem in my question.
Here is a modified version of your ViewController class:
import UIKit
import WebKit
class ViewController: UIViewController, UIScrollViewDelegate, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
#IBOutlet weak var pagesLabel: UILabel!
var readBookNumber = 0
let headerString = "<meta name=\"viewport\" content=\"initial-scale=1.0\" />"
var textSize = 3
var contentSize: CGSize = .zero
override func viewDidLoad() {
super.viewDidLoad()
// Web View Delegate
webView.scrollView.delegate = self
webView.navigationDelegate = self
webView.scrollView.isPagingEnabled = true
webView.scrollView.alwaysBounceVertical = false
webView.scrollView.showsHorizontalScrollIndicator = true
webView.scrollView.showsVerticalScrollIndicator = false
webView.scrollView.panGestureRecognizer.isEnabled = false
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
webView.scrollView.bouncesZoom = false
self.webView.isOpaque = false;
self.webView.backgroundColor = .clear
// Load File
do {
guard let filePath = Bundle.main.path(forResource: "0", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: 4, fontColor: "black")
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
// add content size Observer
webView.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
catch {
print ("File HTML error")
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
let contentSize = webView.scrollView.contentSize
if contentSize != self.contentSize {
self.contentSize = contentSize
DispatchQueue.main.async {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
}
}
// MARK: - webView Scroll View
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.stoppedScrolling()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.stoppedScrolling()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var currentPage = Int((webView.scrollView.contentOffset.x / webView.scrollView.frame.size.width) + 1)
let pageCount = Int(webView.scrollView.contentSize.width / webView.scrollView.frame.size.width)
if currentPage == 0 {
currentPage = 1
} else {
}
if !webView.isHidden {
pagesLabel.text = "\( currentPage ) из \( pageCount )"
} else {
pagesLabel.text = ""
}
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
}
func stoppedScrolling() {
let pageToLoad = Int((webView.scrollView.contentOffset.x))
UserDefaults.standard.set(pageToLoad, forKey: "pageToLoad")
}
// MARK: - loading webView
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Маленькая задержка, которую мне хотелось бы использовать
/*DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}*/
// Большая задержка, которую мне приходится использовать
// don't do this here... we'll do the "auto-scroll" inside the change contentSize Observer
//DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
//}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
}
}
extension String {
mutating func changeHtmlStyle(font: String, fontSize: Int, fontColor: String) {
let style = "<font face='\(font)' size='\(fontSize)' color= '\(fontColor)'>%#"
self = String(format: style, self)
}
}
It uses an Observer to watch the contentSize change in the web view's scroll view.
Note that it is called multiple times - with different values - during the load and layout process, but it may do the job for you.
Also note, though, that you'll need to account for changes in the web view size - for example, if the user rotates the device. So... more to do, but this may get you going.
You can add a property observer and watch the estimated progress of the page load:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
and observe when the page is being loaded:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
print ("page loaded")
}
}
}
You may be able to predict based on the page number how far into the loading process you need to be before you set your offset.
Instead of observing WKWebView.estimatedProgress you should observe UIScrollView.contentSize because you need to scroll to an available position e.g.:
var positionY: CGFloat = 1000
var contentSize = CGSize(width: 0, height: 0)
override func viewDidLoad() {
super.viewDidLoad()
...
webView?.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
if let contentSize = webView?.scrollView.contentSize, contentSize != self.contentSize {
self.contentSize = contentSize
if contentSize.height > positionY {
webView?.scrollView.setContentOffset(CGPoint(x: 0, y: positionY), animated: true)
}
}
}
}

Buttons and some user interactions are not responding with iOS14 app

I am struggling with a strange bug with my iOS app. When using a simulator with iOS 13 application works as it should, but when using iOS 14 buttons, switches, and other functionalities are not responding. There is no error output in the console.
I don't understand why this happens only with XCode 11 and iOS 14.
This a snippet with initializing one of my buttons in the View.
let logButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setLogInButton()
button.setTitle(NSLocalizedString("log_in", comment: ""), for: .normal)
return button
}()
Here I'm assigning the target to the button.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initConstraints()
backgroundColor = UIColor().settingsTableViewCellColor()
logButton.addTarget(self, action: #selector(buttonLogInLogOut), for: .touchUpInside)
}
There is the action
#objc func buttonLogInLogOut(_ sender: UIButton){
print("Log in clicked")
delegate?.logInLogOutFunc(sender)
}
As I said the buttons (switches, and others) are not responding ONLY in iOS 14.
It looks like targetActions are not working.
Thanks for any kind of help.
Regards
Matt
I had the same problem with a button in a table cell not working
For some reason you have to add the button to the contentView of the cell instead of the cell itself as follows
cell.contentView.addSubView(button)
Worked for me afterwards
Just follow this solution
For Cell:
class CommentCell: UICollectionViewCell {
private let commentViewCell: CommentViewCell = {
let view = CommentViewCell()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(commentViewCell)
NSLayoutConstraint.activate([
commentViewCell.leadingAnchor.constraint(equalTo: leadingAnchor),
commentViewCell.trailingAnchor.constraint(equalTo: trailingAnchor),
commentViewCell.topAnchor.constraint(equalTo: topAnchor),
commentViewCell.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setData(item: CommentViewModel) {
commentViewCell.setData(item: item)
}
}
For View:
class CommentViewCell: UIView {
private let lblId: CustomClick = {
let view = CustomClick()
view.backgroundColor = .cyan
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let lblMessage: UILabel = {
let view = UILabel()
view.backgroundColor = .cyan
view.textColor = .black
view.font = .boldSystemFont(ofSize: 16)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let lblDate: UILabel = {
let view = UILabel()
view.backgroundColor = .systemIndigo
view.textColor = .black
view.font = .boldSystemFont(ofSize: 16)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .brown
addSubview(lblId)
addSubview(lblMessage)
addSubview(lblDate)
NSLayoutConstraint.activate([
lblId.topAnchor.constraint(equalTo: topAnchor),
lblId.trailingAnchor.constraint(equalTo: trailingAnchor),
lblId.leadingAnchor.constraint(equalTo: leadingAnchor),
lblMessage.trailingAnchor.constraint(equalTo: trailingAnchor),
lblMessage.leadingAnchor.constraint(equalTo: leadingAnchor),
lblMessage.topAnchor.constraint(equalTo: lblId.bottomAnchor),
lblDate.trailingAnchor.constraint(equalTo: trailingAnchor),
lblDate.leadingAnchor.constraint(equalTo: leadingAnchor),
lblDate.topAnchor.constraint(equalTo: lblMessage.bottomAnchor),
])
lblId.addTarget(self, action: #selector(onClick), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func onClick() {
print("this is click button")
}
func setData(item: CommentViewModel) {
lblId.setData(item: item.id)
lblMessage.text = item.meesage
lblDate.text = item.date
lblDate.textColor = item.cellColor
}
class CustomClick: UIControl {
private let lblId: UILabel = {
let view = UILabel()
view.textColor = .black
view.backgroundColor = .systemPink
view.font = .boldSystemFont(ofSize: 16)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(lblId)
NSLayoutConstraint.activate([
lblId.topAnchor.constraint(equalTo: topAnchor),
lblId.trailingAnchor.constraint(equalTo: trailingAnchor),
lblId.leadingAnchor.constraint(equalTo: leadingAnchor),
lblId.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setData(item: String) {
lblId.text = item
}
}
}

Landscape application only one controller in Portrait - Swift 3

In App Delegate:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask(rawValue: UIInterfaceOrientationMask.landscape.rawValue)
}
In my View Controller(MainViewController) I have added
override func viewDidLoad() {
super.viewDidLoad()
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
supportedInterfaceOrientations()
preferredInterfaceOrientationForPresentation()
// Do any additional setup after loading the view.
}
private func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask(rawValue: UIInterfaceOrientationMask.portrait.rawValue)
}
private func shouldAutorotate() -> Bool {
return true
}
private func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
// Only allow Portrait
return UIInterfaceOrientation.portrait
}
This is the only controller in the application that I want to work in portrait mode. except this everything in Landscape mode.
But I've tried numerous things still unable to understand why is it not working.
Thanks in advance. Sorry for being noob in swift.
write this code in appdelegate
var shouldRotate = false
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if shouldRotate {
return .landscape
}
else {
return .portrait
}
}
set this code to your view controller in viewDidload()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.shouldRotate = true // or false to disable rotation
//you can manage only changing true or false
hope its helps you

Disable swipe gesture in QLPreviewController

I am trying to preview the Images/Video/PDF in UICollectionViewCell (full screen).
for PDF I am trying to use the QuickLook framework.
Here is what I am trying to do,
CollectionView
CollectiViewCell
QLPreviewController.view as subview of CollectionView.contentView
and other cells will be simple UIImageViews.
So when I scroll right/left the touches are always consumed by QLPreviewController and I am unable to navigate to next cell.
I tried adding subview after adding qlpreviewcontroller.view (to intercept touches)
The Implementation follows:
class SampleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
return nil
}
}
class SCGalleryCollectionViewCell: UICollectionViewCell,UIScrollViewDelegate, QLPreviewControllerDataSource {
var quickLookController : PreviewController!
var scrollView : UIScrollView!
var imageView : SCImageView!
override init(frame: CGRect) {
super.init(frame: frame)
let sampleView = SampleView()
sampleView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(sampleView)
sampleView.fillHorizontally()
sampleView.fillVertically()
sampleView.backgroundColor = UIColor.greenColor()
contentView.bringSubviewToFront(sampleView)
quickLookController = PreviewController()
quickLookController.dataSource = self
// quickLookController.view.frame = CGRectMake(0, 0, 100, 100)
quickLookController.view.translatesAutoresizingMaskIntoConstraints = false
sampleView.addSubview(quickLookController.view)
quickLookController.view.fillVertically()
quickLookController.view.fillHorizontally()
quickLookController.view.userInteractionEnabled = false
quickLookController.view.gestureRecognizers = nil
}
}
class PreviewController: QLPreviewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.userInteractionEnabled = false
view.exclusiveTouch = false
view.multipleTouchEnabled = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return false
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
}
So my question is, how can I disable the swipe gesture on qlpreviewcontroller ?
Thanks in advance!!