I created a custom Numpad keyboard through xib and wanted to add a blur effect to its background. So I add a Visual Effect View in xib: https://i.stack.imgur.com/QjiwP.png
Main View and Visual Effect View background color is set to Default and I also tried to use Clear Color.
The problem is when I initialize the Numpad, background has a light grey color without any blur effect: https://i.stack.imgur.com/LebUA.png
How to add a blur effect to the Numpad so yellow square can be blurred and visible?
Code for NumpadView:
import UIKit
class NumpadView: UIView {
@IBOutlet weak var resetButton: NumpadButton!
@IBOutlet weak var decimalButton: NumpadButton!
var target: UITextInput?
var view: UIView?
init(target: UITextInput, view: UIView) {
super.init(frame: .zero)
self.target = target
self.view = view
initializeSubview()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initializeSubview()
}
func initializeSubview() {
let xibFileName = "NumpadView"
let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
self.addSubview(view)
view.frame = self.bounds
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
Code for initializing in a VC:
import UIKit
class NumpadViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
textField.inputView = NumpadView(target: textField, view: view)
}
}
Post
Replies
Boosts
Views
Activity
I have a converter which converts numbers. When user is typing a number from NumberPad keyboard into my cell textField I want it to be formatted and displayed in real time like:
1000 -> 1 000
10000 -> 10 000
1000000 -> 1 000 000
1000,77 -> 1 000,77
I know it should happen in textFieldDidChangeSelection method. Here is mine:
func textFieldDidChangeSelection(_ textField: UITextField) {
numberFromTextField = Double(textField.text!)
//This is for reload visible Rows in my tableView. Might not needed in my question's context.
let activeTextFieldIndexPath = IndexPath(row: textField.tag, section: 0)
var visibleIndexPaths = [IndexPath]()
for indexPath in tableView.indexPathsForVisibleRows! {
if indexPath != activeTextFieldIndexPath {
visibleIndexPaths.append(indexPath)
}
}
tableView.reloadRows(at: visibleIndexPaths, with: .none)
}
There I have a global variable numberFromTextField which I made because I need to use the Double version of textField.text in my separate calculation methods.
How can I implement above formatted behaviour and at the same time save numberFromTextField = Double(textField.text!) as I need it in my different calculations?
I have a textField where I can type numbers (2 333,44) or simple math equations (12+3, 11x3,5). For now I can modify the textField.text with adding a new string only to the end of existing text. If I move cursor to any position inside number it will add a new character only at the end: https://i.stack.imgur.com/HzaA9.gif
How to modify the code to insert new characters at any cursor position?
My current code from shouldChangeCharactersIn:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 1
formatter.locale = .current
formatter.roundingMode = .down
let numberString = "\(textField.text ?? "")".replacingOccurrences(of: formatter.groupingSeparator, with: "")
let lastCharacter = numberString.last ?? "."
var symbol: String {
var tempArray = [String]()
let mathSymbols = "+-÷x"
for character in numberString {
if mathSymbols.contains(character) {
tempArray.append(String(character))
}
}
return tempArray.last ?? ""
}
var numbersArray: [String] {
if numberString.first == "-" {
let positiveString = numberString.dropFirst()
var tempArray = positiveString.components(separatedBy: symbol)
tempArray[0] = "-\(tempArray[0])"
return tempArray
} else {
return numberString.components(separatedBy: symbol)
}
}
//turn numbers into Float and create a String from them to be able to receive a correct result from NSExpression
var floatNumberArray: [Float] {
if numberString.first == "-" {
let tempString = lastCharacter == Character(formatter.decimalSeparator) ? numberString.dropFirst().dropLast() : numberString.dropFirst()
var tempArray = tempString.components(separatedBy: symbol)
tempArray[0] = "-\(tempArray[0])"
return tempArray.compactMap { Float($0.replacingOccurrences(of: formatter.decimalSeparator, with: ".")) }
} else {
return numberString.components(separatedBy: symbol).compactMap { Float($0.replacingOccurrences(of: formatter.decimalSeparator, with: ".")) }
}
}
var floatNumberString: String {
if numberString.contains("x") {
return "\(floatNumberArray.first ?? 0)\("*")\(floatNumberArray.last ?? 0)"
} else if numberString.contains("÷") {
return "\(floatNumberArray.first ?? 0)\("/")\(floatNumberArray.last ?? 0)"
} else {
return "\(floatNumberArray.first ?? 0)\(symbol)\(floatNumberArray.last ?? 0)"
}
}
let amountOfDecimals = "\(numbersArray.last ?? "")\(string)".filter({ $0 == Character(formatter.decimalSeparator) }).count
//allow to insert 0 after the decimal symbol to avoid formatting i.e. 2.04
formatter.minimumFractionDigits = numberString.last == Character(formatter.decimalSeparator) && string == "0" ? 1 : 0
//allow string to be modified by backspace button
if string == "" { return false }
//allow numbers as a first character, except math symbols
if numberString == "" {
if Character(string).isNumber {
textField.text = string
} else {
return false
}
}
//allow only one decimal symbol per number
else if amountOfDecimals > 1 { return false }
if numbersArray.count > 1 {
//if number is entered
if Character(string).isNumber {
textField.text = "\(formatter.string(for: Double("\(numbersArray.first?.replacingOccurrences(of: formatter.decimalSeparator, with: ".") ?? "")") ?? 0) ?? "")\(symbol)\(formatter.string(for: Double("\(numbersArray.last?.replacingOccurrences(of: formatter.decimalSeparator, with: ".") ?? "")\(string)") ?? 0) ?? "")"
//if symbol is entered
} else if string == formatter.decimalSeparator {
textField.text = "\(textField.text ?? "")\(string)"
} else {
//perform calculation if last entered character is a number
if lastCharacter.isNumber {
let result = performCalculation(format: floatNumberString)
textField.text = string == "=" ? "\(formatter.string(from: result) ?? "")" : "\(formatter.string(from: result) ?? "")\(string)"
//perform calculation if last entered character is a decimal symbol
} else if lastCharacter == Character(formatter.decimalSeparator) {
let result = performCalculation(format: floatNumberString)
textField.text = string == "=" ? "\(formatter.string(from: result) ?? "")" : "\(formatter.string(from: result) ?? "")\(string)"
//change math symbol before enter a second number
} else {
textField.text = "\(textField.text?.dropLast() ?? "")\(string)"
}
}
} else {
//if number is entered
if Character(string).isNumber {
textField.text = "\(formatter.string(for: Double("\(numbersArray.first?.replacingOccurrences(of: formatter.decimalSeparator, with: ".") ?? "")\(string)") ?? 0) ?? "")"
} else {
//if math symbol is entered
if lastCharacter.isNumber {
textField.text = "\(textField.text ?? "")\(string)"
}
}
}
return false
}
func performCalculation(format: String) -> NSNumber {
let expression = NSExpression(format: format)
let answer = expression.expressionValue(with: nil, context: nil)
return answer as! NSNumber
}
}
My guess is to modify the string with a range. So I tried:
1.Create range
let numberString = "\(textField.text ?? "")".replacingOccurrences(of: formatter.groupingSeparator, with: "")
guard let range = Range(range, in: numberString) else { return false }
2.Remove string at the end and use range instead
} else {
//if number is entered
if Character(string).isNumber {
textField.text = "\(formatter.string(for: Double("\(numbersArray.first?.replacingOccurrences(of: formatter.decimalSeparator, with: ".").replacingCharacters(in: range, with: string) ?? "") ?? "")") ?? 0) ?? "")"
} else {
//if math symbol is entered
if lastCharacter.isNumber {
textField.text = "\(textField.text?.replacingCharacters(in: range, with: string) ?? "") ?? "")"
}
}
}
return false
}
3.Then I can insert at any position but can add only 4 numbers if add from keyboard and insert more from another cursor position: https://i.stack.imgur.com/iDV4n.gif
Test project on GitHub
I have a custom textField's input view - it is a Numpad style keyboard. Numpad is using to add numbers and math symbols to a textField.
I can't figure out how can I change a math symbol in a string if user already add one and wants to change it on another straight away. Here is an example of what I need: https://i.stack.imgur.com/IVGId.gif
Here is the code I use:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//number formatter
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.locale = .current
formatter.roundingMode = .down
//all possible math operation symbols user can add
let symbolsSet = Set(["+","-","x","/"])
var amountOfSymbols = 0
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
//current math symbol user add
let symbol = symbolsSet.filter(completeString.contains).last ?? ""
//if user add math symbol to an empty string - do not insert
if string == symbol, numberString.count == 0 { return false }
//count how much math symbols string has. If more that one - do not insert, string can have only one
completeString.forEach { character in
if symbolsSet.contains(String(character)) {
amountOfSymbols += 1
}
}
if amountOfSymbols > 1 { return false }
//count how much decimals string has. If more that one - do not insert because it can have only one per number
let numbersArray = completeString.components(separatedBy: symbol)
for number in numbersArray {
let amountOfDecimalSigns = number.filter({$0 == "."}).count
if amountOfDecimalSigns > 1 { return false }
}
//create numbers from a string
guard let firstNumber = Double(String(numbersArray.first ?? "0")) else { return true }
guard let secondNumber = Double(String(numbersArray.last ?? "0")) else { return true }
//format numbers and turn them back to string
let firstFormattedNumber = formatter.string(for: firstNumber) ?? ""
let secondFormattedNumber = formatter.string(for: secondNumber) ?? ""
//assign formatted numbers to a textField
textField.text = completeString.contains(symbol) ? "\(firstFormattedNumber)\(symbol)\(secondFormattedNumber)" : "\(firstFormattedNumber)"
return string == formatter.decimalSeparator
}
The logic for me was to use textField.deleteBackwards() method to delete an old one and add a new math symbol after, but with above code it doesn't work: it deletes symbol, but a new one doesn't appear - I should press again so new symbol can appear.
What should I do to change a math symbol in a string?
Test project on GitHub
I'm trying to format numbers in a UITextField consists of math equation String: "number + number".
At the moment I can type just a single number, then convert it to Double -> format with NSNumberFormatter -> convert back to String -> assign to textField.text: https://i.stack.imgur.com/4qQza.gif
The code:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.locale = .current
formatter.roundingMode = .down
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
guard let value = Double(completeString) else { return false }
let formattedNumber = formatter.string(for: value)
textField.text = formattedNumber
return string == formatter.decimalSeparator
}
Now I want to add a calculation functionality and display a simple math equation in a textField as "number + number", but each number should be formatted as shown on above screenshot. Example (but without formatting): https://i.stack.imgur.com/W7Jet.gif
I can't properly implement that. The logic for me was: track the String each time new char inserts -> if it has math sign extract numbers -> convert them to Double -> format with NSNumberFormatter -> convert back to String -> construct a new String "number + number".
The code I tried:
if let firstString = completeString.split(separator: "+").first, let secondString = completeString.split(separator: "+").last {
guard let firstValue = Double(firstString) else { return false }
guard let secondValue = Double(secondString) else { return false }
let firstFormattedNumber = formatter.string(for: firstValue)
let secondFormattedNumber = formatter.string(for: secondValue)
textField.text = "\(firstFormattedNumber ?? "") + \(secondFormattedNumber ?? "")"
// another try
if completeString.contains("+") {
let stringArray = completeString.components(separatedBy: "+")
for character in stringArray {
print(character)
guard let value = Double(character) else { return false }
guard let formattedNumber = formatter.string(for: value) else { return false }
textField.text = "\(formattedNumber) + "
}
}
But it's not working properly. I tried to search but didn't find any similar questions.
Test project on GitHub
How can I format the numbers from such a string?
My app used to rely on standard iOS Numpad Keyboard as a textField.inputView. The textField could group typed numbers (1000 -> 1 000, 50067 -> 50 067 etc) and limit the amount of numbers after the decimal point as 2 max: https://i.stack.imgur.com/69LVr.gif (GIF)
I've created a custom numpad keyboard view and implement its logic with UITextInput protocol according to that guide on StackOverflow.
I can't understand of how to deal with the code I used for grouping and limit numbers with standard iOS Numpad. Because it doesn't work properly after I type numbers from my new custom Numpad (only 2 numbers): https://i.stack.imgur.com/f3u3n.gif (GIF)
The code I use for grouping and limit numbers after decimal point in a textField:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.groupingSeparator = " "
formatter.maximumFractionDigits = 2
let textString = textField.text ?? ""
guard let range = Range(range, in: string) else { return false }
let updatedString = textString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: ",", with: ".")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
guard completeString.count <= 12 else { return false }
guard !completeString.isEmpty else { return true }
textField.text = completeString
return string == formatter.decimalSeparator
}
Custom NumpadView:
class NumpadView: UIView {
var target: UITextInput?
init(target: UITextInput) {
super.init(frame: .zero)
self.target = target
initializeSubview()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initializeSubview()
}
func initializeSubview() {
let xibFileName = "NumpadView"
let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
self.addSubview(view)
view.frame = self.bounds
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
@IBAction func buttonPressed(_ sender: NumpadButton) {
insertText((sender.titleLabel!.text)!)
}
func insertText(_ string: String) {
guard let range = target?.selectedRange else { return }
if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
return
}
target?.insertText(string)
}
}
extension UITextInput {
var selectedRange: NSRange? {
guard let selectedRange = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: selectedRange.start)
let length = offset(from: selectedRange.start, to: selectedRange.end)
return NSRange(location: location, length: length)
}
}
2.How I initialize it in a VC:
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
textField.inputView = NumpadView(target: textField)
}
Test project on GitHub: CLICK
What should I do so the code in shouldChangeCharactersIn method work for my custom Numpad?
I have implemented a feature, when you press on a UITabBar icon and viewController1 scrolls up using its UIScrollView. It works perfectly, but if I scroll view down and stop somewhere, then switch to another viewController2, then get back to viewController1 and press tabBar icon - the viewController1 will scroll up, but Large Title will never be showed, and I should press tabBar icon one more time to show it:
Here is a GIF with behaviour: https://i.stack.imgur.com/VfP4o.gif
The code I use for scroll up the VC1:
private var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(view.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if tabBarController.selectedIndex == 0 {
let navigationVC = viewController as? UINavigationController
let firstVC = navigationVC?.viewControllers.first as? CurrencyViewController
guard let scrollView = firstVC?.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView else { return }
if traitCollection.verticalSizeClass == .compact {
scrollView.setContentOffset(CGPoint(x: 0, y: -view.safeAreaInsets.top, animated: true)
} else {
scrollView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset, animated: true)
}
}
}
I tried to track biggestTopSafeAreaInset in different stages of VC1 life, but it always has the same number - 196.0. But then why it doesn't scroll till the Large Title after viewControllers switch?
I need to make 2 API calls simultaneously. I have 2 URLs for the calls, and if one of the calls will return any error I want to stop all the code execution.
How I tried to do it:
I have a function called performRequest() with a completion block. I call the function in my ViewController to update the UI - show an error/or a new data if all was successful. Inside it I create a URLSession tasks and then parse JSON:
I created an array with 2 urls:
func performRequest(_ completion: @escaping (Int?) -> Void) {
var urlArray = [URL]()
guard let urlOne = URL(string: "https://api.exchangerate.host/latest?base=EUR&places=9&v=1") else { return }
guard let urlTwo = URL(string: "https://api.exchangerate.host/2022-05-21?base=EUR&places=9") else { return }
urlArray.append(urlOne)
urlArray.append(urlTwo)
}
Then for each of the url inside the array I create a session and a task:
urlArray.forEach { url in
let session = URLSession(configuration: .ephemeral)
let task = session.dataTask(with: url) { data, _, error in
if error != nil {
guard let error = error as NSError? else { return }
completion(error.code)
return
}
if let data = data {
let printData = String(data: data, encoding: String.Encoding.utf8)
print(printData!)
DispatchQueue.main.async {
self.parseJSON(with: data)
}
}
}
task.resume()
}
print("all completed")
completion(nil)
}
For now I receive print("all completed") printed once in any situation: if both tasks were ok, if one of them was ok or none of them.
What I want is to show the print statement only if all tasks were completed successfully and to stop executing the code if one of them returned with error (for example if we will just delete one of the symbols in url string which will take it impossible to receive a data).
How can I do it correctly?
I have created a User Onboarding as a Collection View with 5 cells (pages).
I have 2 types of Collection View Items: welcomeCell which is always a first and single cell in Onboarding (indexPath.item == 0) and tutorialCell (all other cells) which has a tableView and can scroll its content vertically.
I want to add the next behaviour:
user scrolls tableView content and swipes to the next page
if user swipes back to the page he scrolled I want the page layout to be reloaded like it was initially set.
For now if a user swipes to the page he scrolled, he will see its position where he ended the scroll.
I suggests that collectionView content can be reloaded in didEndDisplaying method:
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item != 0 {
self.collectionView.reloadItems(at: [indexPath])
}
}
But with this code I receive a strange cells behaviour: I can swipe up tableView on one page and the came content position will be on another page, but some page can be loaded with normal content position. Here is a GIF: https://cln.sh/wgnjS8
I also tried collectionView.reloadData() in scrollViewDidScroll and scrollViewDidEndDecelerating but receive the similar behaviour.
Would you be so kind to share some experience and help to understand what's the best practice for my case? Maybe I should find a way to not reload a page content at all, but reload imageView height anchor in constraints?
I have created a User Onboarding as a Collection View with 5 cells (pages).
The Collection View has a UIPageControl which shows an active page user currently on and 2 UIButtons (previous and next) which needed to manually scroll the pages if user don't want to swipe.
Here is how I manage the buttons IBAction when user tap:
@IBAction func prevButtonClicked(_ sender: UIButton) {
if currentPage != 0 {
currentPage -= 1
let indexPath = IndexPath(item: currentPage, section: 0)
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
@IBAction func nextButtonClicked(_ sender: UIButton) {
if currentPage == slides.count - 1 {
//hide onboarding
} else {
currentPage += 1
let indexPath = IndexPath(item: currentPage, section: 0)
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Also if user swipes a page instead of tap on buttons I use scrollViewDidScroll() method to update UIPageControl dot:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRectangle = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let visiblePoint = CGPoint(x: visibleRectangle.midX, y: visibleRectangle.midY)
currentPage = collectionView.indexPathForItem(at: visiblePoint)?.row ?? 0
}
The currentPage is a computed property:
private var currentPage = 0 {
didSet {
pageControl.currentPage = currentPage
currentPage == 0 ? hidePreviousButton() : showPreviousButton()
}
}
I have a problem: when tap on buttons I force collectionView to scroll and update currentPage, therefore scrollViewDidScroll called and currentPage updates again.
Because of that when I tap on buttons I can see that UIPageControl dot and backButton are flicker since the code runs twice:
didSet {
pageControl.currentPage = currentPage
currentPage == 0 ? hidePreviousButton() : showPreviousButton()
}
Here is a GIF with the problem: https://cln.sh/ffXG9A
How can I avoid the double call to scrollViewDidScroll when tap on buttons?
I have made an Onboarding View using a collectionView with 2 items.
When I change device orientation from landscape to portrait the second item being animated and visible within the first item screen borders.
You can see the problem here: https://cln.sh/sgypvH
How to fix the animation so when switching from landscape to portrait I could see only the first item content with a white background on the screen?
Here is my code for collectionViewLayout:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
Here is the code for willTransition:
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView.collectionViewLayout.invalidateLayout()
let indexPath = IndexPath(item: self.currentPage, section: 0)
DispatchQueue.main.async {
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Please let me know if you need anything extra to help me to solve the issue. Thank you!
I have 4 sections, each section have 2 nested rows. I open the rows by tapping on each section.
Here is how my initial data looks like. It has title, subtitle and options (which is what nested rows should display):
private var sections = [
SortingSection(title: "По имени", subtitle: "Российский рубль", options: ["По возрастанию (А→Я)", "По убыванию (Я→А)"]),
SortingSection(title: "По короткому имени", subtitle: "RUB", options: ["По возрастанию (А→Я)", "По убыванию (Я→А)"]),
SortingSection(title: "По значению", subtitle: "86,22", options: ["По возрастанию (1→2)", "По убыванию (2→1)"]),
SortingSection(title: "Своя", subtitle: "в любом порядке", options: ["Включить"])
]
When I tap on a section I want it accessory (chevron.right, made as UIImageView) be rotated in sync with expanding of nested rows and when I click again the same behaviour for closing.
I have a variable called isOpened (bool, false by default), which I change from false to true and back each tap in didSelectRowAt. Based on that a show all nested cells and rotate the UIImageView:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
sections[indexPath.section].isOpened.toggle()
guard let cell = tableView.cellForRow(at: indexPath) as? MainSortTableViewCell else { return }
UIView.animate(withDuration: 0.3) {
if self.sections[indexPath.section].isOpened {
cell.chevronImage.transform = CGAffineTransform(rotationAngle: .pi/2)
} else {
cell.chevronImage.transform = .identity
}
} completion: { _ in
tableView.reloadSections([indexPath.section], with: .none)
}
}
As you can see above I reload tableView section to show\hide nested rows in a completion block after animation. I can't use reloadSections in an if\else statement because then chevron animation gets skipped.
Also my numberOrRowsInSection method:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
if section.isOpened {
return section.options.count + 1
} else {
return 1
}
}
Here is how it looks now: https://cln.sh/3hjI2q
Here is what I want (any iPhone native apps): https://cln.sh/z0LM6E
I tried to add and delete rows instead of reloading the whole section, but always end up with error:
UIView.animate(withDuration: 0.3) {
if self.sections[indexPath.section].isOpened {
cell.chevronImage.transform = CGAffineTransform(rotationAngle: .pi/2)
for i in 0..<self.sections[indexPath.section].options.count {
tableView.insertRows(at: [IndexPath(row: 1+i, section: indexPath.section)], with: .none)
}
} else {
cell.chevronImage.transform = .identity
for i in 0..<self.sections[indexPath.section].options.count {
tableView.deleteRows(at: [IndexPath(row: i-1, section: indexPath.section)], with: .none)
}
}
}
How can I change my code to solve the task and animate chevron at the same time nested rows expand or close?
I've implemented a popup notification in my app to show user the data was being updated successfully or not.
This popup is just a UIView instantiated from .xib. Here is screenshot of elements hierarchy in xib file: https://cln.sh/te8JhM
Also I wanted the ability to swipe out the popup if user don't want to see a full time it shows itself on the screen. To do that I have implemented a UIGestureRecognizer:
private func configureTapGesture() {
guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
swipe.direction = .up
window.addGestureRecognizer(swipe)
}
@objc private func handleSwipes(_ sender:UISwipeGestureRecognizer) {
if sender.direction == .up {
UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
self.popupView.center.y -= 50
} completion: { _ in
self.popupView.removeFromSuperview()
}
self.isRemovedByTap = true
}
}
When I run the app it works only if I swipe out anywhere but not inside the popupView. Please check for the GIF: https://cln.sh/sNm5RN
If I replace a target from self (Popup class) to popupView (UIVisualEffectView, which is a rounded rectangle you see on GIF), then I receive an error unrecognized selector sent to instance
Here is a my full custom Popup class where I initialize the view, configure, animate and show it:
import UIKit
class PopupView: UIView {
@IBOutlet weak var popupView: UIVisualEffectView!
@IBOutlet weak var symbol: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
private var isRemovedByTap = false
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
private func configure() {
if let views = Bundle.main.loadNibNamed("PopupView", owner: self) {
guard let view = views.first as? UIView else { return }
view.frame = bounds
addSubview(view)
}
}
private func configurePopup() {
guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
popupView.layer.cornerRadius = 20
popupView.clipsToBounds = true
popupView.center.x = window.frame.midX
popupView.translatesAutoresizingMaskIntoConstraints = true
window.addSubview(popupView)
}
private func animatePopup() {
UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
self.popupView.center.y += 34
} completion: { _ in
UIView.animate(withDuration: 0.15, delay: 10.0, options: .curveLinear) {
self.popupView.center.y -= 50
} completion: { _ in
if !self.isRemovedByTap {
self.popupView.removeFromSuperview()
}
}
}
}
private func configureTapGesture() {
guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
swipe.direction = .up
window.addGestureRecognizer(swipe)
}
@objc private func handleSwipes(_ sender:UISwipeGestureRecognizer) {
if sender.direction == .up {
UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
self.popupView.center.y -= 50
} completion: { _ in
self.popupView.removeFromSuperview()
}
self.isRemovedByTap = true
}
}
func showPopup(title: String, message: String, symbol: UIImage) {
titleLabel.text = title
descriptionLabel.text = message
self.symbol.image = symbol
configurePopup()
animatePopup()
configureTapGesture()
}
}
What should I change to be able to swipe out only when inside the popupView rounded rectangle?
I want to implement a popup notification into my app when data was being updated successfully or not. To do that I:
created a .xib file: Screenshot - https://cln.sh/HvEHiX
created a class where I load that NIB:
import UIKit
class PopupView: UIView {
static let instance = PopupView()
@IBOutlet weak var backgroundView: UIView!
@IBOutlet weak var popupView: UIVisualEffectView!
@IBOutlet weak var symbol: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
Bundle.main.loadNibNamed("PopupView", owner: self)
popupView.layer.cornerRadius = 20
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showPopup(title: String, message: String, symbol: UIImage, on viewController: UIViewController) {
self.titleLabel.text = title
self.descriptionLabel.text = message
self.symbol.image = symbol
guard let targetView = viewController.view else { return }
backgroundView.frame = targetView.bounds
targetView.addSubview(backgroundView)
}
In the above class I created a showPopup method where defined a backgroundView frame to be equal to ViewController bounds.
When I call that method in desired ViewController I receive the behaviour where my popupView shows itself and then went off the screen straight away (black area in the GIF): GIF - https://cln.sh/e4ukf4
Would you be so kind to help me understand and fix the reason why the popupView went off the screen and not just equal to a ViewController bounds.
I am building a Settings screen using Storyboard and UITableViewController with Static Cells.
Here is a full view hierarchy and constraints I use for every cell in my Settings Table View: https://cln.sh/tAmFbl
The problem appears when I switch portrait mode on landscape mode or switch to device with a smaller screen. Starting to receive an errors like:
Ambiguous Layout: Position and size are ambiguous for "Stack View".
And:
Stack View, need constraints for: X position, width
Stack View, need constraints for: Y position, height
Here is a picture of the behaviour: https://cln.sh/fq6kOK
If I switch back to portrait mode all errors goes away...
How can I fix that?