I have a
class Fancy:UIButton
and I want to find all the sibling views which are the same class.
I do this
for v:UIView in superview!.subviews
{
if v.isKindOfClass(Fancy)
{
// you may want... if (v==self) continue
print("found one")
(v as! Fancy).someProperty = 7
(v as! Fancy).someCall()
}
}
it seems to work reliably in testing (no siblings, many, etc)
But there's a lot of "!" in there.
Is this the right way in Swift?
BTW here's a cool way to do it with extensions based on the great answers below
Pass in a type to a generic Swift extension, or ideally infer it
What about using functional programming?
self.superview?
.subviews
.flatMap { $0 as? Fancy }
.filter { $0 != self }
.forEach { fancy in
fancy.someProperty = 4
fancy.someMethod()
}
What about:
for v in superview!.subviews
{
if let f = v as? Fancy{
print("found one")
f.someProperty = 7
f.someCall()
}
}
Or this:
if let views = superview?.subviews
{
for aView in views
{
if let fancyView = aView as? Fancy
{
fancyView.someProperty = 7
fancyView.someCall()
}
}
}
#RobMayoff has a good point about excluding self. The code really should be:
if let views = superview?.subviews
{
for aView in views
{
if let fancyView = aView as? Fancy where fancyView != self
{
fancyView.someProperty = 7
fancyView.someCall()
}
}
}
Related
I am creating a wizard using UICollectionView with an array of CollectionViewCells:
var viewCells:[BaseCVCell] = [createEventSubjectSearch(), createEventEventForm()]
This array is dynamically added to based on a series of UISwitch's that the user controls. I can add to the array fine using the code below, however I can't seem to remove an item when a user turns the switch off.
func switchToggled(sender : UISwitch) {
if sender == createDiarySwitch {
if sender.isOn {
parentClass?.viewCells.append(createEventDeferEvent())
} else {
if let i = parentClass?.viewCells.index(where: { $0 == createEventDeferEvent() }) {
parentClass?.viewCells.remove(at: i)
}
}
}
if sender == createDeferredSwitch {
if sender.isOn {
parentClass?.viewCells.append(createEventDiariseEvent())
} else {
if let i = parentClass?.viewCells.index(where: { $0 == createEventDiariseEvent() }) {
parentClass?.viewCells.remove(at: i)
}
}
}
parentClass?.wizardCollectionView.reloadData()
}
I have tried the above code, as well as:
if let index = parentClass?.viewCells.index(of: createEventDiariseEvent()) {
parentClass?.viewCells.remove(at: index)
}
Neither approach works (no errors, the code just never returns a value). I'd like to try and avoid naming elements where possible. Is there a way to do this?
Thanks for your answers, DonMag
I've achieved the desired functionality by instanciating the two dynamic cells in the main class:
let diariseCell : createEventDiariseEvent()
and then in the loop calling as thus:
if sender == createDiarySwitch {
if sender.isOn {
parentClass?.viewCells.append((parentClass?.diariseCell)!)
} else {
if let i = parentClass?.viewCells.index(where: { $0 == parentClass?.diariseCell }) {
print("Found cell reference at index \(i)")
parentClass?.viewCells.remove(at: i)
}
}
}
Works a charm now. Amazing what another pair of eyes can pick out!
I am trying for a few days now to get this converted into Swift without really having much background with it.
This is what I've got so far ... and I have been looking on google not really knowing what to search for in order to be more specific. Can you please shed some light on what I'm doing wrong ? Thanks
Update:
I have aded the objective-c tag just so more people that are related to this thread may be able to see it and hopefully get an answer.
For those who are still looking, the WebKit team updated WKWebView (iOS 13+) so that you can subclass it to remove/update the input accessory view:
https://trac.webkit.org/changeset/246229/webkit#file1
In Swift, I subclassed it, and returned nil. Worked as expected. I hope it helps.
FYI: I checked the docs, and it doesn't mention not to subclass WKWebView, so subclassing is allowed.
import WebKit
class RichEditorWebView: WKWebView {
var accessoryView: UIView?
override var inputAccessoryView: UIView? {
// remove/replace the default accessory view
return accessoryView
}
}
You can find a working version of it here: https://github.com/cbess/RichEditorView/commits/master
Michael Dautermann answer has got everything right, but in order to hide the accessory bar you need to swizzle the method inputAccessoryView() of UIView Class with the inputAccessoryView() of the _NoInputAccessoryView class. I have just added the couple of extra lines to the code which does this job of method swizzling.
First you'll need a fake class to swap with
final class FauxBarHelper: NSObject {
var inputAccessoryView: AnyObject? { return nil }
}
Then create this method in your controller class
/// Removes the keyboard accessory view from the web view
/// Source: http://stackoverflow.com/a/32620344/308315 / http://stackoverflow.com/a/33939584/308315
func _removeInputAccessoryView(webView: UIWebView) {
var targetView: UIView? = nil
for view in webView.scrollView.subviews {
if String(describing: type(of: view)).hasPrefix("WKContent") {
targetView = view
}
}
guard let target = targetView else { return }
let noInputAccessoryViewClassName = "\(target.superclass!)_NoInputAccessoryView"
var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil {
let targetClass: AnyClass = object_getClass(target)
newClass = objc_allocateClassPair(targetClass, noInputAccessoryViewClassName.cString(using: String.Encoding.ascii)!, 0)
}
let originalMethod = class_getInstanceMethod(FauxBarHelper.self, #selector(getter: FauxBarHelper.inputAccessoryView))
class_addMethod(newClass!.self, #selector(getter: FauxBarHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
object_setClass(target, newClass)
}
HTH ;)
Here's a slightly safer (no unsafe unwraps) version that works with Swift 4 and (at least) iOS 9 trough 12.
fileprivate final class InputAccessoryHackHelper: NSObject {
#objc var inputAccessoryView: AnyObject? { return nil }
}
extension WKWebView {
func hack_removeInputAccessory() {
guard let target = scrollView.subviews.first(where: {
String(describing: type(of: $0)).hasPrefix("WKContent")
}), let superclass = target.superclass else {
return
}
let noInputAccessoryViewClassName = "\(superclass)_NoInputAccessoryView"
var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil, let targetClass = object_getClass(target), let classNameCString = noInputAccessoryViewClassName.cString(using: .ascii) {
newClass = objc_allocateClassPair(targetClass, classNameCString, 0)
if let newClass = newClass {
objc_registerClassPair(newClass)
}
}
guard let noInputAccessoryClass = newClass, let originalMethod = class_getInstanceMethod(InputAccessoryHackHelper.self, #selector(getter: InputAccessoryHackHelper.inputAccessoryView)) else {
return
}
class_addMethod(noInputAccessoryClass.self, #selector(getter: InputAccessoryHackHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
object_setClass(target, noInputAccessoryClass)
}
}
This code snippet should get you over your issue:
class _NoInputAccessoryView: NSObject {
func removeInputAccessoryViewFromWKWebView(webView: WKWebView) {
// make sure to make UIView an optional here...
var targetView: UIView? = nil
for view in webView.scrollView.subviews {
if String(view.dynamicType).hasPrefix("WKContent") {
targetView = view
}
}
// only optionals can be nil
if targetView == nil {
return
}
let noInputAccessoryViewClassName = "\(targetView!.superclass)_NoInputAccessoryView"
var newClass : AnyObject? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil {
let uiViewClass : AnyClass = object_getClass(targetView!)
newClass = objc_allocateClassPair(uiViewClass, noInputAccessoryViewClassName.cStringUsingEncoding(NSASCIIStringEncoding)!, 0)
}
}
You can also use "String(view.dynamicType)" to get the class name of the object you're looking at, as I noticed via this answer as I was researching the way to solve your problem.
Using hasPrefix like that in both Objective-C and Swift is really hacky and perhaps a better way of hiding the keyboard could be found for production code?
I would like to find the first EKSource of type EKSourceType.Local with a "single"-line expression in Swift. Here is what I currently have:
let eventSourceForLocal =
eventStore.sources[eventStore.sources.map({ $0.sourceType })
.indexOf(EKSourceType.Local)!]
Is there a better way of doing this (such as without mapping and/or with a generic version of find)?
Alternatively in Swift3 you could use:
let local = eventStore.sources.first(where: {$0.sourceType == .Local})
There's a version of indexOf that takes a predicate closure - use it to find the index of the first local source (if it exists), and then use that index on eventStore.sources:
if let index = eventStore.sources.indexOf({ $0.sourceType == .Local }) {
let eventSourceForLocal = eventStore.sources[index]
}
Alternately, you could add a generic find method via an extension on SequenceType:
extension SequenceType {
func find(#noescape predicate: (Self.Generator.Element) throws -> Bool) rethrows -> Self.Generator.Element? {
for element in self {
if try predicate(element) {
return element
}
}
return nil
}
}
let eventSourceForLocal = eventStore.sources.find({ $0.sourceType == .Local })
(Why isn't this there already?)
I don't understand why you're using map at all. Why not use filter? You will then end up with all the local sources, but in actual fact there will probably be only one, or none, and you can readily find out by asking for the first one (it will be nil if there isn't one):
let local = eventStore.sources.filter{$0.sourceType == .Local}.first
Swift 4 solution that also handles the situation when there are no elements in your array that match your condition:
if let firstMatch = yourArray.first{$0.id == lookupId} {
print("found it: \(firstMatch)")
} else {
print("nothing found :(")
}
Swift 5 If you want to find out from Array of Model then speciyfy $0.keyTofound otherwise use $0
if let index = listArray.firstIndex(where: { $0.id == lookupId }) {
print("Found at \(index)")
} else {
print("Not found")
}
Let's try something more functional:
let arr = [0,1,2,3]
let result = arr.lazy.map { print("💥"); return $0 }.first(where: { $0 == 2 })
print(result) // 3x 💥 then 2
Whats cool about this?
You get access to element or i while you search. And it's functional.
For Swift 3 you'll need to make a few small changes to Nate's answer above. Here's the Swift 3 version:
public extension Sequence {
func find(predicate: (Iterator.Element) throws -> Bool) rethrows -> Iterator.Element? {
for element in self {
if try predicate(element) {
return element
}
}
return nil
}
}
Changes: SequenceType > Sequence, Self.Generator.Element > Iterator.Element
I am aware of quite a few posts that say this should not be done, or is not possible. I tinkered with a few ideas and now I'm asking this question, because I want to be absolutely sure there are no other options.
Option 1:
The most popular solution is to change AppleLanguages as in this post. I do not mind the idea of requiring a restart, so this would be an acceptable solution for me, except that you cannot restart your app programmatically (can't find the method, or would be rejected by Apple). Asking the user to manually restart the application wouldn't be ideal.
Option 2:
The next solution is to get the appropriate bundle and perform a localizedStringForKey lookup on each and every UILabel, UIButton, etc. This can be a little tedious but is okay for me, since I already added localizationProperties (similar to this) to these views so that I can have a centralized strings file.
AppDelegate.swift:
static var userLanguage: String?
{
set
{
let defaults = NSUserDefaults.standardUserDefaults();
defaults.setObject(newValue, forKey: LanguageKey);
defaults.synchronize();
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle());
instance.window?.rootViewController = storyboard.instantiateInitialViewController();
}
get
{
let defaults = NSUserDefaults.standardUserDefaults();
return defaults.stringForKey(LanguageKey);
}
}
Localization.swift:
private var bundle: NSBundle
{
get
{
let bundle: NSBundle;
#if TARGET_INTERFACE_BUILDER
bundle = NSBundle(forClass: self.dynamicType);
#else
bundle = NSBundle.mainBundle();
#endif
let lang: String;
if(AppDelegate.userLanguage == nil || AppDelegate.userLanguage == "en")
{
lang = "Base";
}
else
{
lang = AppDelegate.userLanguage!;
}
let path = bundle.pathForResource(lang, ofType: "lproj");
if(path != nil)
{
let toreturn = NSBundle(path: path!);
if(toreturn != nil)
{
return toreturn!;
}
}
return bundle;
}
}
extension UILabel
{
#IBInspectable var localizedText: String?
{
get { return "" }
set
{
if(newValue != nil)
{
text = bundle.localizedStringForKey(newValue!, value:"", table: nil);
}
}
}
}
The problem with option 2 is that this only sets the language, for those fields. Layout direction will be unchanged, and files such as language specific layouts would not be used.
By extending UIApplication I am able to specify a custom userInterfaceLayoutDirection which successfully swaps all layouts between LTR and RTL.
DemoApplication.swift:
class DemoApplication: UIApplication
{
override internal var userInterfaceLayoutDirection: UIUserInterfaceLayoutDirection
{
get
{
if(AppDelegate.userLanguage == "ar")
{
return UIUserInterfaceLayoutDirection.RightToLeft;
}
return UIUserInterfaceLayoutDirection.LeftToRight;
}
}
}
Now when I set AppDelegate.userLanguage the application will reset to the initial view controller, displaying the new language, flipping the layout between LTR and RTL. This does not address the issue of language specific files, and I've also noticed that text remains left or right aligned within its own bounds.
Since I can't find the source code for native iOS classes, I can't see what language specific variables are set at startup so I assumed it is linked to the NSBundle.mainBundle. I tried to override it by using method swizzling.
extension NSBundle
{
override public class func initialize()
{
struct Static
{
static var token: dispatch_once_t = 0;
}
// make sure this isn't a subclass
if (self !== NSBundle.self)
{
return;
}
dispatch_once(&Static.token)
{
do
{
try jr_swizzleClassMethod("mainBundle", withClassMethod: "mainBundleExt");
}
catch
{
print("\(error)");
}
}
super.initialize();
}
public class func mainBundleExt() -> NSBundle
{
let bundle = self.mainBundleExt(); // Due to swizzling, this is the "super" method
let lang: String;
if(AppDelegate.userLanguage == nil || AppDelegate.userLanguage == "en")
{
lang = "Base";
}
else
{
lang = AppDelegate.userLanguage!;
}
let path = bundle.pathForResource(lang, ofType: "lproj");
if(path != nil)
{
let toreturn = NSBundle(path: path!);
if(toreturn != nil)
{
return toreturn!;
}
}
}
}
This does not work though, it seems as though the default mainBundle is still used.
So my question is this: How is mainBundle assigned a value? Which other language specific variables are set at startup, such as userInterfaceLayoutDirection.
I assume there are 2 or 3 of these variables. Finally, is it possible for this to work or am I just wasting my time?
Thanks.
I had this problem before and I have used a library. it helped me to change the language on the fly.
try to use this:
https://github.com/Decybel07/L10n-swift
L10n.shared.language = "en"
L10n.shared.language = "en-GB"
At runtime, you can switch the language at any time by setting the language property
Use this line of code it will change layout without closing application. From right to left
UIView.appearance().semanticContentAttribute = .forceRightToLeft
And for Left to Right Flip
UIView.appearance().semanticContentAttribute = .forceLeftToRight
and if you want to change textfield layout or text change then use this code because i faced this issue . textfield's texts was not changning layout. check this code to change layout of textfield text
extension UITextField {
open override func awakeFromNib() {
super.awakeFromNib()
if UserDefaults.languageCode == "ar" {
if textAlignment == .natural {
self.textAlignment = .right
}
}
}
}
I have a custom class of buttons in a UIView that I'd like to add to an array so that they're easily accessible. Is there a way to get all subviews of a specific class and add it to an array in Swift?
The filter function using the is operator can filter items of a specific class.
let myViews = view.subviews.filter{$0 is MyButtonClass}
MyButtonClass is the custom class to be filtered for.
To filter and cast the view to the custom type use compactMap
let myViews = view.subviews.compactMap{$0 as? MyButtonClass}
Here you go
extension UIView {
/** This is the function to get subViews of a view of a particular type
*/
func subViews<T : UIView>(type : T.Type) -> [T]{
var all = [T]()
for view in self.subviews {
if let aView = view as? T{
all.append(aView)
}
}
return all
}
/** This is a function to get subViews of a particular type from view recursively. It would look recursively in all subviews and return back the subviews of the type T */
func allSubViewsOf<T : UIView>(type : T.Type) -> [T]{
var all = [T]()
func getSubview(view: UIView) {
if let aView = view as? T{
all.append(aView)
}
guard view.subviews.count>0 else { return }
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
}
You can call it like
let allSubviews = view.allSubViewsOf(type: UIView.self)
let allLabels = view.allSubViewsOf(type: UILabel.self)
So many of the answers here are unnecessarily verbose or insufficiently general. Here's how to get all subviews of a view, at any depth, that are of any desired class:
extension UIView {
func subviews<T:UIView>(ofType WhatType:T.Type) -> [T] {
var result = self.subviews.compactMap {$0 as? T}
for sub in self.subviews {
result.append(contentsOf: sub.subviews(ofType:WhatType))
}
return result
}
}
How to use:
let arr = myView.subviews(ofType: MyButtonClass.self)
To do this recursively (I.e. fetching all subview's views aswell), you can use this generic function:
private func getSubviewsOf<T : UIView>(view:UIView) -> [T] {
var subviews = [T]()
for subview in view.subviews {
subviews += getSubviewsOf(view: subview) as [T]
if let subview = subview as? T {
subviews.append(subview)
}
}
return subviews
}
To fetch all UILabel's in a view hierarchy, just do this:
let allLabels : [UILabel] = getSubviewsOf(view: theView)
I can't test it right now but this should work in Swift 2:
view.subviews.flatMap{ $0 as? YourView }
Which returns an array of YourView
Here's a tested, typical example, to get a count:
countDots = allDots!.view.subviews.flatMap{$0 as? Dot}.count
From Swift 4.1, you can use new compactMap (flatMap is now depcrecated): https://developer.apple.com/documentation/swift/sequence/2950916-compactmap
(see examples inside)
In your case, you can use:
let buttons:[UIButton] = stackView.subviews.compactMap{ $0 as? UIButton }
And you can execute actions to all buttons using map:
let _ = stackView.subviews.compactMap{ $0 as? UIButton }.map { $0.isSelected = false }
If you want to update/access those specific subviews then use this,
for (index,button) in (view.subviews.filter{$0 is UIButton}).enumerated(){
button.isHidden = false
}
func allSubViews(views: [UIView]) {
for view in views {
if let tf = view as? UITextField {
// Do Something
}
self.allSubViews(views: view.subviews)
}
}
self.allSubViews(views: self.view.subviews)
For this case, I think we could use Swift's first.where syntax, which is more efficient than filter.count, filter.isEmpty.
Because when we use filter, it will create a underlying array, thus not effective, imagine we have a large collection.
So just check if a view's subViews collection contains a specific kind of class, we can use this
let containsBannerViewKind = view.subviews.first(where: { $0 is BannerView }) != nil
which equivalent to: find me the first match to BannerView class in this view's subViews collection. So if this is true, we can carry out our further logic.
Reference: https://github.com/realm/SwiftLint/blob/master/Rules.md#first-where
Let me post my variation of this) but this, finds the first of T
extension UIView {
func firstSubView<T: UIView>(ofType type: T.Type) -> T? {
var resultView: T?
for view in subviews {
if let view = view as? T {
resultView = view
break
}
else {
if let foundView = view.firstSubView(ofType: T.self) {
resultView = foundView
break
}
}
}
return resultView
}
}
Swift 5
func findViewInside<T>(views: [UIView]?, findView: [T] = [], findType: T.Type = T.self) -> [T] {
var findView = findView
let views = views ?? []
guard views.count > .zero else { return findView }
let firstView = views[0]
var loopViews = views.dropFirst()
if let typeView = firstView as? T {
findView = findView + [typeView]
return findViewInside(views: Array(loopViews), findView: findView)
} else if firstView.subviews.count > .zero {
firstView.subviews.forEach { loopViews.append($0) }
return findViewInside(views: Array(loopViews), findView: findView)
} else {
return findViewInside(views: Array(loopViews), findView: findView)
}
}
How to use:
findViewInside(views: (YourViews), findType: (YourType).self)
I've gone through all the answers above, they cover the scenario where the views are currently displayed in the window, but don't provide those views which are in view controllers not shown in the window.
Based on #matt answers, I wrote the following function which recursively go through all the views, including the non visible view controllers, child view controllers, navigation controller view controllers, using the next responders
(Note: It can be definitively improved, as it adds more complexity on top of the recursion function. consider it as a proof of concept)
/// Returns the array of subviews in the view hierarchy which match the provided type, including any hidden
/// - Parameter type: the type filter
/// - Returns: the resulting array of elements matching the given type
func allSubviews<T:UIView>(of type:T.Type) -> [T] {
var result = self.subviews.compactMap({$0 as? T})
var subviews = self.subviews
// *********** Start looking for non-visible view into view controllers ***********
// Inspect also the non visible views on the same level
var notVisibleViews = [UIView]()
subviews.forEach { (v) in
if let vc = v.next as? UIViewController {
let childVCViews = vc.children.filter({$0.isViewLoaded && $0.view.window == nil }).compactMap({$0.view})
notVisibleViews.append(contentsOf: childVCViews)
}
if let vc = v.next as? UINavigationController {
let nvNavVC = vc.viewControllers.filter({$0.isViewLoaded && $0.view.window == nil })
let navVCViews = nvNavVC.compactMap({$0.view})
notVisibleViews.append(contentsOf: navVCViews)
// detect child vc in not visible vc in the nav controller
let childInNvNavVC = nvNavVC.compactMap({$0.children}).reduce([],+).compactMap({$0.view})
notVisibleViews.append(contentsOf: childInNvNavVC)
}
if let vc = v.next as? UITabBarController {
let tabViewControllers = vc.viewControllers?.filter({$0.isViewLoaded && $0.view.window == nil }) ?? [UIViewController]()
// detect navigation controller in the hidden tab bar view controllers
let vc1 = tabViewControllers.compactMap({$0 as? UINavigationController})
vc1.forEach { (vc) in
let nvNavVC = vc.viewControllers.filter({$0.isViewLoaded && $0.view.window == nil })
let navVCViews = nvNavVC.compactMap({$0.view})
notVisibleViews.append(contentsOf: navVCViews)
// detect child vc in not visible vc in the nav controller
let childInNvNavVC = nvNavVC.compactMap({$0.children}).reduce([],+).compactMap({$0.view})
notVisibleViews.append(contentsOf: childInNvNavVC)
}
// ad non-navigation controller in the hidden tab bar view controllers
let tabVCViews = tabViewControllers.compactMap({($0 as? UINavigationController) == nil ? $0.view : nil})
notVisibleViews.append(contentsOf: tabVCViews)
}
}
subviews.append(contentsOf: notVisibleViews.removingDuplicates())
// *********** End looking for non-visible view into view controllers ***********
subviews.forEach({result.append(contentsOf: $0.allSubviews(of: type))})
return result.removingDuplicates()
}
extension Array where Element: Hashable {
func removingDuplicates() -> [Element] {
var dict = [Element: Bool]()
return filter { dict.updateValue(true, forKey: $0) == nil }
}
}
Sample usage:
let allButtons = keyWindow.allSubviews(of: UIButton.self)
Note: If a modal view controller is currently presented, the above script does not find views which are contained in the presentingViewController. (Can be expanded for that, but I could not find an elegant way to achieve it, as this code is already not elegant by itself :/ )
Probably is not common to have this need, but maybe helps someone out there :)