I have a storyboard with some text views on it. When I try to localize the storyboard (creating Storyboard.strings file), all of the UITextViews don't localize at all. UILabels are fine.
My config is as follow:
1 Base Storyboard file with 2 Storyboard.strings files
Storyboard Configuration
Storyboard.strings files are ok, its working with UILabels after all:
Storyboard.strings file
Have you found a solution for this problem?
I'm using Xcode 8.3, Swift 3, iOS 10.
You could just make a IBOutlet in Xcode and set the initial UITextView value in the didSet function.
E.g.
#IBOutlet var textView: UITextView {
didSet {
textView.text = NSLocalizedString("CUSTOM_LOCALISED_STRING", comment: "Comment.")
}
}
I ended up with this solution:
In a UIViewController I can access my UITextView-objects and force the localized text from Storyboard.strings to be used.
func fixTextViewStoryboardLocalization() {
guard storyboard != nil else {
return // No need to fix it if storyboard is not presented
}
let storyboardName = storyboard!.value(forKey: "name") as! String
for case let textView as UITextView in view.subviews {
if let ident = textView.restorationIdentifier {
textView.text = NSLocalizedString("\(ident).text", tableName: storyboardName, bundle: Bundle.main, value: "", comment: "")
}
}
}
Create a custom MyViewController (pick whatever name you want), call this function in viewDidLoad:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fixTextViewStoryboardLocalization()
}
}
Make sure the storyboard view controller is from class MyViewController:
MyViewController custom class in Storyboard
In order the code above to work, I need to set the Restoration ID for each UITextView in my Storyboard:
UITextView Restoration ID
Final step - localize the text view in Storyboard.strings:
/* Class = "UITextView"; text = "Base text"; ObjectID = "MyTextView-Restoration-ID"; */
"MyTextView-Restoration-ID.text" = "Localized text comes here";
This works for all my UITextViews in all my storyboards.
Related
I tried looking for a solution in posts such as this and this where people asked this very same question: How to share a url to Facebook using SwiftUI?
I even tried this post where somebody asked how to export a file using SwiftUI, but my problem is specifically with Facebook since I have no problem sharing urls to apps such as Whatsapp.
However, I found no answers...
UIKit
By reading Facebook's Developer documentation, I found a way to share a post using UIKit.
I created a very simple sample project to make sure I understood the topic.
Here is the sample project in UIKit:
import UIKit
import FBSDKShareKit
class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func share(_ sender: UIButton) {
shareLink(url: URL(string: "http://www.apple.com")!)
}
func shareLink(url: URL) {
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
fromViewController: self,
content: content,
delegate: nil
)
dialog.show()
}
}
Luckily, this worked right away.
By making this project work, I confirmed that I wasn't forgetting to link my AppBundleID to Facebook, or forgetting to add my FacebookAppID or any other needed files inside my info.plist.
SwiftUI
I then tried to replicate the same project in SwiftUI to see if I could get it to work.
I used ViewControllerRepresentable to be able to include UIActivityViewController into SwiftUI.
The reason for using UIActivityViewController is because I want the user to choose where they want to share the URL (Whatsapp, Twitter, Facebook, etc).
Here is the code:
ContentView
struct ContentView: View {
#State var showSharingView = false
var body: some View {
Button("Share Link") {
showSharingView.toggle()
}
.sheet(isPresented: $showSharingView) {
ActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!])
}
}
}
UIViewControllerRepresentable
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [URL]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
if activityType == .postToFacebook {
shareLink(from: activityItems.first!)
}
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func shareLink(from url: URL) {
// controller was created so I would have a UIViewControllerType to put as a parameter for fromViewController in ShareDialog, even though I don't think it fits
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(fromViewController: controller, content: content, delegate: nil)
dialog.show()
}
}
This code doesn't work because of what I assume is the controller variable used when initializing ShareDialog. (self doesn't work either because "XCode cannot convert value of type 'ActivityViewController' to expected argument type 'UIViewController?'")
Question
Facebook Developer's documentation tells me that I need to write the following code in order to share a link:
guard let url = URL(string: "https://developers.facebook.com") else {
// handle and return
}
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
viewController: self, //<--this has been changed to 'fromViewController'
content: content,
delegate: self
)
dialog.show()
However, SwiftUI doesn't work with ViewControllers. How can I create a ViewController to use as a parameter in fromViewController in ShareDialog in order to successfully share my URL to Facebook?
Use like this,
guard let url = URL(string: "https://developers.facebook.com") else {
// handle and return
}
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
viewController: UIApplication.shared.windows.first!.rootViewController,
content: content,
delegate: UIApplication.shared.windows.first!.rootViewController
)
dialog.show()
Multiline text input is currently not natively supported in SwiftUI (hopefully this feature is added soon!) so I've been trying to use the combine framework to implement a UITextView from UIKit which does support multiline input, however i've been having mixed results.
This is the code i've created to make the Text view:
struct MultilineTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.backgroundColor = UIColor.white
view.textColor = UIColor.black
view.font = UIFont.systemFont(ofSize: 17)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
func frame(numLines: CGFloat) -> some View {
let height = UIFont.systemFont(ofSize: 17).lineHeight * numLines
return self.frame(height: height)
}
func makeCoordinator() -> MultilineTextView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultilineTextView
init(_ parent: MultilineTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
I've then implemented it in a swiftUI view like:
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)//.frame(numLines: 4)
And bound it to a state variable:
#State var currentItem:Item
It sort of works. However, the state var currentItem:Item contains an array of strings which I'm then cycling through using buttons which update the string array based on what has been inputted into MultilineTextView. This is where i'm encountering a problem where the MultilineTextView seems to bind to only the first string item in the array, and then it won't change. When I use swiftUI's native TextField view this functionality works fine and I can cycle through the string array and update it by inputting text into the TextField.
I think I must be missing something in the MultilineTextView struct to allow this functionality. Any pointers are gratefully received.
Update: Added model structs
struct Item: Identifiable, Codable {
let id = UUID()
var completed = false
var pairArray:[TextPair]
}
struct TextPair: Identifiable, Codable {
let id = UUID()
var textOne:String
var textTwo:String
}
Edit:
So I've done some more digging and I've found what I think is the problem. When the textViewDidChange of the UITextView is triggered, it does send the updated text which I can see in the console. The strange thing is that the updateUIView function then also gets triggered and it updates the UITextView's text with what was in the binding var before the update was sent via textViewDidChange. The result is that the UITextview just refuses to change when you type into it. The strange thing is that it works for the first String in the array, but when the item is changed it won't work anymore.
It appears that SwiftUI creates two variants of UIViewRepresentable, for each binding, but does not switch them when state, ie title is switched... probably due to defect, worth submitting to Apple.
I've found worked workaround (tested with Xcode 11.2 / iOS 13.2), use instead explicitly different views as below
if title {
MultilineTextView(text: $currentItem.titleEnglish)
} else {
MultilineTextView(text: $currentItem.pairArray[currentPair].textOne)
}
So I figured out the problem in the end, the reason why it wasn't updating was because I was passing in a string which was located with TWO state variables. You can see that in the following line, currentItem is one state variable, but currentPair is another state variable that provides an index number to locate a string. The latter was not being updated because it wasn't also being passed into the multiline text view via a binding.
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)
I thought initially that passing in one would be fine and the parent view would handle the other one but this turns out not to be the case. I solved my problem by making two binding variables so I could locate the string that I wanted in a dynamic way. Sounds stupid now but I couldn't see it at the time.
I have this scenario:
1) When app starts, the app show the SplashScreen.
2) I need this splashScreen to run a simple logic
I am confused with the following:
Problem:
How to make SplashScreen with logic?
I add a new CocoaTouch file and call it SplashVC.swift
and assign it to the UIViewController in the LaunchScreen.storyboard.
**at LaunchScreen.storyboard:**
I see there is an arrow on the left of the ViewController.
This means it is the first VC to be loaded.
**at Main.storyboard**
I have a MainVC which I made it as the first Page to be loaded.
There are two VC which are the First Page to be loaded.
So, how do I do the the following tasks for LuanchScreen for the below :
conditions:
if there is a value in UserDefaults, go to VC1
if there is no value in UserDefaults, got to VC2
|- viewLogin
SplashScreen.swift ----|
|- viewMain
Update
1) Delete the Default LaunchScreen called: LaunchScreen.storyboard
1.1 ) add interface file in the project
in file dialogBox: select the user interface file of type : Launch Screen
name it : splash (it will create splash.storyboard)
2) Add a CocoaTouch File and Call it SplashVC.swift
3) Assign this SplashVC.swift to UIViewController in Splash.Storyboard
4) Click this UIViewController and goto File Inspector
at Interface Builder Document
untick :Use as LaunchScreen
5) click the project and goto General
Goto App Icons and Launch Images section <br/>
Launch screen file : Splash.storyboard<br/>
6) I have the viewControllers I want to navigate to base on Condition in splashVC.swift:
viewLogin.swift
viewMain.swift
7) Add code in SplashVC.swift
func checkLogin()
{
var mainView: UIStoryboard!
mainView = UIStoryboard(name: "MainStoryboard", bundle: nil)
var viewcontroller : UIViewController
if UserDefaults.standard().bool(forKey: "login"){
viewcontroller = mainView.instantiateViewController(withIdentifier: "viewLogin") as! viewLogin
}else {
viewcontroller = mainView.instantiateViewController(withIdentifier: "viewMain") as! viewMain
}
//--- error : value of type SplashVC has no member 'window'
self.window!.rootViewController = viewcontroller
self.window?.makeKeyAndVisible();
}
}
Error :
//--- error : value of type SplashVC has no member 'window'
self.window!.rootViewController = viewcontroller
self.window?.makeKeyAndVisible();
Please help. Thanks
Thanks
it is very simple just remove the launch screen story board and add your own.
just follow the
step:1 add your own storyboard with single view controller and assign
it to your own splashvc class.
step:2 make your splash storyboard as main interface
now you can do any coding in splash like
class SplashVC: UIViewController {
override func viewDidLoad(){
super.viewDidLoad();
checkLogin()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
}
func checkLogin()
{
var mainView: UIStoryboard!
mainView = UIStoryboard(name: "MainStoryboard", bundle: nil)
var viewcontroller : UIViewController;
if UserDefaults.standard().bool(forKey: "login"){
viewcontroller = mainView.instantiateInitialViewController()!;
}else {
viewcontroller = mainView.instantiateViewController(withIdentifier: "yourSecondVcIde") as! YourSecondVC
}
self.window!.rootViewController = viewcontroller
self.window?.makeKeyAndVisible();
}
}
as we know, to implement PageTabBarController, we need to insert these code in AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
let viewControllers = [MatchDetailViewController(),ListPlayersViewController(),ChatViewController()]
window = UIWindow(frame: Device.bounds)
window!.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
window!.makeKeyAndVisible()
}
Now, i need to use PageTabBarController when i want to open detail for my match data. My question is, how to implement it without insert those code in AppDelegate.swift because it will open my MatchViewController (extend from PageTabBarController) for the first app launch.
I have tried this code, but it will cause Crash, and it pointed to my AppDelegate.swift
class MatchViewController: PageTabBarController {
var window: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
open override func prepare() {
super.prepare()
let viewControllers = [MatchDetailViewController(),ListPlayersViewController(),ChatViewController()]
//1st try: Crash
window = UIWindow(frame: Device.bounds)
window!.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
window!.makeKeyAndVisible()
//2nd try: error
self.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
//3rd try: crash
self.viewControllers = viewControllers
delegate = self
preparePageTabBar()
}
fileprivate func preparePageTabBar() {
pageTabBar.lineColor = Color.blue.base
pageTabBar.dividerColor = Color.blueGrey.lighten5
pageTabBarAlignment = PageTabBarAlignment.top
pageTabBar.lineAlignment = TabBarLineAlignment.bottom
}
}
extension MatchViewController: PageTabBarControllerDelegate {
func pageTabBarController(_ pageTabBarController: PageTabBarController, didTransitionTo viewController: UIViewController) {
}
}
Linked GitHub Question
Hi, yes there is a way. The PageTabBarController is inherited from aUIViewController`, which allows you to add it as a child of any other UIViewController. That said, you just gave me a great idea. I am going to make a new UIViewController that allows you to add as many child UIViewControllers, which will make this super easy to do. I will make this as a Feature Request.
Until the update, please use the suggested method of adding it as a child UIViewController. Are you familiar with how to do that?
First create AppToolbarController (subclass of ToolbarController) or you can use the one in the Material library demo.
And then from your view controller, you can use:
DispatchQueue.main.async {
let tabbarViewController = AppPageTabbarController(viewControllers: [vc1,vc2,vc3], selectedIndex: 0)
self.present(AppToolbarController(rootViewontroller: tabbarViewController))
}
I've tried to expand the default Apple MasterDetail Template by adding a UITabbarController in front of the UINavigationController of the MasterView, so there is a structure like this:
UISplitViewController (Master) > UITabbarController > UINavigationController > UITableViewController
But if I run the App, after changing application(didFinishLaunchingWithOptions) to use the correct ViewController, and try to perform the ShowDetails Segue the DetailsView ist presented Modally on the iPhone. On the other side the iPad Version is working as expected.
What am I forgot to do? Or how can I fix it?
I figured out how to put the detail on to the master's UINavigationController instead of presenting it modally over the UITabBarController.
Using the UISplitViewControllerDelegate method
- splitViewController:showDetailViewController:sender:
In case the UISplitViewController is collapsed get the masters navigation controller and push the detail view onto this navigation controller:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
showDetailViewController:(UIViewController *)vc
sender:(id)sender {
NSLog(#"UISplitViewController collapsed: %d", splitViewController.collapsed);
// TODO: add introspection
if (splitViewController.collapsed) {
UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;
// push detail view on the navigation controller
[masterNavigationController pushViewController:vc animated:YES];
return YES;
}
return NO;
}
Just to update the answers above. Since you can't push navigation controllers anymore, you have to push its top view controller instead.
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
if splitViewController.collapsed {
let tabBarController = splitViewController.viewControllers.first as! UITabBarController
let selectedNavigationViewController = tabBarController.selectedViewController as! UINavigationController
// Push view controller
var viewControllerToPush = vc
if let navController = vc as? UINavigationController {
viewControllerToPush = navController.topViewController
}
selectedNavigationViewController.pushViewController(viewControllerToPush, animated: true)
return true
}
return false
}
Here's my solution. Place in MasterViewController.m and remember to give your detail view a Storyboard ID in IB. In my case 'detail'.
-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"showDetail"] && self.splitViewController.collapsed) {
DetailViewController *myController = (DetailViewController *)[self.storyboard instantiateViewControllerWithIdentifier:#"detail"];
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
[myController setDetailItem:object];
[self.navigationController showViewController:myController sender:self];
return NO;
}
return YES;
}
There is another way to do it without code.
After you embedded the the UINavigationController in the TabBarController embed the TabBarController in another UINavigationController. So you will have: SplitViewController -> Master -> NavCon -> TabBar -> NavCon -> TableViewController.
It's much easier doing like this, but there a bug that I haven't found out how to fix. The navigation bar presented will be that of the TabBarController, not the TableViewController. Any ideas how to fix that?
Subclass TabBarController like this:
- (void)showViewController:(UIViewController *)vc sender:(id)sender
{
if ([self.selectedViewController isKindOfClass:UINavigationController.class])
[self.selectedViewController showViewController:vc sender:sender];
else
[super showViewController:vc sender:sender];
}
- (UIViewController*)separateSecondaryViewControllerForSplitViewController:(UISplitViewController *)splitViewController
{
return [self.selectedViewController separateSecondaryViewControllerForSplitViewController:splitViewController];
}
- (void)collapseSecondaryViewController:(UIViewController *)secondaryViewController forSplitViewController:(UISplitViewController *)splitViewController
{
[self.selectedViewController.navigationController collapseSecondaryViewController:secondaryViewController forSplitViewController:splitViewController];
}
See this question for complete explanation.
Here is an alternative that is based on testing the size classes of the splitViewController :
Use a custom UISplitViewController (subclass)
Override the showDetailViewController operation
Use the traitCollection to determine the class of the UISplitViewController
If the horizontal class is Compact, get the navigationController to call showViewController
Here is the the code of the custom UISplitViewController :
import UIKit
class CustomSplitViewController: UISplitViewController {
override func showDetailViewController(vc: UIViewController!, sender: AnyObject!) {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact) {
if let tabBarController = self.viewControllers[0] as? UITabBarController {
if let navigationController = tabBarController.selectedViewController as? UINavigationController {
navigationController.showViewController(vc, sender: sender)
return
}
}
}
super.showDetailViewController(vc, sender: sender)
}
}
Do not forget to the set the custom class in the storyboard.
Tested in the simulator of iPhone 6, iPhone 6+ and iPad Air and worked as expected.