【问题标题】:How can I Implement Hide/Show password style experience for iOS with UITextView in Xamarin?如何在 Xamarin 中使用 UITextView 实现 iOS 的隐藏/显示密码样式体验?
【发布时间】:2019-10-15 12:38:06
【问题描述】:

我知道UITextFields可以使用SecureTextEntry属性实现密码风格的效果,我也在网上找到了一些代码来实现隐藏/显示密码效果here,但这仅适用于@987654326 @,我需要为自定义 UITextView 实现相同的功能。我目前有一些代码来实现为UI添加图像,但没有实现实际的显示/隐藏密码效果。

我在Swifthere 中找到了一些关于如何执行此操作的代码,但我从未与Swift 合作过,希望熟悉Swift 的人可以为我将其翻译成C# ,因为这可能是我需要的解决方案。

我也明白,虽然SecureTextEntry 属性在设置为 true 时为UITextFields 提供密码风格的体验(即防止复制并将字符变成黑点),它仅在为 UITextViews 设置为 true 时防止复制文本。这是我从这个属性的文档中找到的 here

这是我目前在文件中实现显示/隐藏密码效果的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ResolutionGroupName("Xamarin")]
[assembly:ExportEffect(typeof(MyApp.iOS.CustomRenderers.PasswordEffect), "PasswordEffect")]
namespace MyApp.iOS.CustomRenderers
{
    public class PasswordEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            Configure();
        }

        protected override void OnDetached()
        {
            //do nothing
        }

        private void Configure()
        {
            if (Control != null)
            {
                if (Control is UITextView) {
                    UITextView vUpdatedEntry = (UITextView)Control;
                    var buttonRect = UIButton.FromType(UIButtonType.Custom);
                    buttonRect.SetImage(UIImage.FromBundle("eye_image"), UIControlState.Normal);
                    buttonRect.TouchUpInside += (object sender, EventArgs e1) => {
                        if (vUpdatedEntry.SecureTextEntry)
                        {
                            vUpdatedEntry.SecureTextEntry = false;
                            buttonRect.SetImage(UIImage.FromBundle("eye_crossed_image"), UIControlState.Normal);
                        }
                        else
                        {
                            vUpdatedEntry.SecureTextEntry = true;
                            buttonRect.SetImage(UIImage.FromBundle("eye_image"), UIControlState.Normal);
                        }
                    };
                    // Would love to have password effect here :)
                    vUpdatedEntry.ShouldChangeText += (textField, range, replacementString) => {
                        string text = vUpdatedEntry.Text;
                        var result = text.Substring(0, (int)range.Location) + replacementString + text.Substring((int)range.Location + (int)range.Length);
                        vUpdatedEntry.Text = result;
                        return false;
                    };

                    buttonRect.Frame = new CoreGraphics.CGRect(10.0f, 0.0f, 15.0f, 15.0f);
                    buttonRect.ContentMode = UIViewContentMode.ScaleToFill;

                    UIView paddingViewRight = new UIView(new System.Drawing.RectangleF(0.0f, 0.0f, 30.0f, 18.0f));
                    paddingViewRight.AddSubview(buttonRect);

                    buttonRect.TranslatesAutoresizingMaskIntoConstraints = false;
                    buttonRect.CenterYAnchor.ConstraintEqualTo(paddingViewRight.CenterYAnchor).Active = true;

                    vUpdatedEntry.TextContainerInset = new UIEdgeInsets(8.0f, 0.0f, 8.0f, paddingViewRight.Frame.Width+5.0f);
                    vUpdatedEntry.AddSubview(paddingViewRight);

                    paddingViewRight.TranslatesAutoresizingMaskIntoConstraints = false;
                    paddingViewRight.TrailingAnchor.ConstraintEqualTo(vUpdatedEntry.LayoutMarginsGuide.TrailingAnchor, 9.0f).Active = true;
                    paddingViewRight.HeightAnchor.ConstraintEqualTo(vUpdatedEntry.HeightAnchor).Active = true;
                    paddingViewRight.WidthAnchor.ConstraintEqualTo(buttonRect.WidthAnchor,1.0f, 0.0f).Active = true;

                    Control.Layer.CornerRadius = 4;
                    Control.Layer.BorderColor = new CoreGraphics.CGColor(255, 255, 255);
                    Control.Layer.MasksToBounds = true;
                    vUpdatedEntry.TextAlignment = UITextAlignment.Left;
                }
            }
        }

    }
}

【问题讨论】:

    标签: c# ios swift xamarin xamarin.ios


    【解决方案1】:

    你可以定义一个自定义的UITextViewDelegate来实现它。

    public class TextViewDelegate : UITextViewDelegate
    {
        private UITextView myTextView;
        bool secureTextViewEntry;       // default is NO
        NSMutableString secureText;
        NSTimer timer;
        NSString lastText;
        public TextViewDelegate(UITextView myTextView)
        {
            this.myTextView = myTextView; 
            secureTextViewEntry = false;
            secureText = new NSMutableString();
        }
    
        public override bool ShouldChangeText(UITextView textView, NSRange range, string text)
        {
            if("\n" == text)
            {
                textView.ResignFirstResponder();
                return false;
            }
            lastText = new NSString(text);
            return true;
            //return base.ShouldChangeText(textView, range, text);
        }
    
        public override void Changed(UITextView textView)
        {
            Console.WriteLine("-----" + secureText);
            if (secureTextViewEntry)
            {
                string text = textView.Text;
                if(text.Length > 0)
                {
                    if("" == lastText)
                    {
                        secureText.DeleteCharacters(new NSRange(secureText.Length - 1, 1));
                        onlyPassword();
                        if (null != timer)
                        {
                            timer.Invalidate();
                        }
                        //base.Changed(textView);
                        return;
                    }
                    else
                    {
                        NSString one = new NSString(text.Substring(text.Length - 1));
                        secureText.Append(one);
                        NSMutableString temp = new NSMutableString();
                        for (int i = 0; i < secureText.Length - 1; i++)
                        {
                            temp.Append(new NSString("•"));
                        }
                        temp.Append(new NSString(secureText.ToString().Substring(secureText.ToString().Length - 1)));
                        myTextView.Text = temp;
    
                        if (null != timer)
                        {
                            timer.Invalidate();
                        }
                        timer =NSTimer.CreateScheduledTimer(2, onlyPassword);
                    }
                }
                else
                {
                    secureText = new NSMutableString();
    
                }
                //base.Changed(textView);
            }
            else {
                if (textView.Text.Length == 0)
                {
                    secureText = new NSMutableString();
                }
                else
                {
                    secureText = new NSMutableString();
                    secureText.SetString(new NSString(textView.Text));
                }
                if(null != timer) {
                    timer.Invalidate();
                }
    
                //base.Changed(textView);
            }
        }
    
    
        private void onlyPassword(NSTimer obj)
        {
            //throw new NotImplementedException();
            onlyPassword();
        }
    
        private void onlyPassword()
        {
            //throw new NotImplementedException();
            timer.Invalidate();
            NSMutableString temp = new NSMutableString();
            for(int i = 0; i< secureText.Length; i++)
            {
                temp.Append(new NSString("•"));
            }
            myTextView.Text = temp;
        }
    
        public override void DidChange(NSKeyValueChange changeKind, NSIndexSet indexes, NSString forKey)
        {
            base.DidChange(changeKind, indexes, forKey);
        }
        //set Secure be true or false
        public void setSecureTextViewEntry(bool _secureTextViewEntry)
        {
            secureTextViewEntry = _secureTextViewEntry;
            if (secureText.Length == 0)
            {
                return;
            }
            else
            {
                if (secureTextViewEntry)
                {
                    //secret
                    NSMutableString aaa = new NSMutableString();
                    for (int i = 0; i < secureText.Length; i++)
                    {
                        aaa.Append(new NSString("•"));
                    }
                    myTextView.Text = aaa;
                }else{
                    //real word
                    myTextView.Text = secureText;
                    if (null != timer)
                    {
                        timer.Invalidate();
                    }
                }
            }
            Changed(myTextView);
        }
    
        public bool getSecureTextViewEntry()
        {
            return secureTextViewEntry;
        }
    }
    

    在 ViewController 中:

    TextViewDelegate textViewDelegate;
    
    public override void ViewDidLoad ()
    {
         base.ViewDidLoad ();
    
         textViewDelegate = new TextViewDelegate(MyTextView);
         MyTextView.Delegate = textViewDelegate; //MyTextView from StoryBoard
    
    }
    
    partial void SetTrueButton_TouchUpInside(UIButton sender)
    {
        textViewDelegate.setSecureTextViewEntry(true);
    }
    
    partial void SetFalseButton_TouchUpInside(UIButton sender)
    {
        textViewDelegate.setSecureTextViewEntry(false);
    }
    

    效果如下:

    注意:代码仅供参考,代码中有一些小问题需要改进。

    【讨论】:

    • 感谢您的回答。输出看起来与我想要的结果非常相似。但是,我无法将此解决方案应用于我的情况。我已经在使用自定义UITextView,并且我有一个类来实现此功能作为效果,(我正在使用Control 为我的自定义UITextView 定义此效果),我希望你能解释一下您的代码可以进一步工作(可能在您的答案中有一些 cmets)。因为我不完全了解自定义代表是如何工作的,我希望我不必使用一个:)
    • @Kikanye Okey,首先我想知道哪个项目是(Forms 或 Xamarin.IOS)。第二种使用delegate不需要修改你太多的代码。你只需要在Xamarin.ios中创建一个TextViewDelegate文件,然后把这个delegate分配给你使用的TextFiled,如果Forms会使用Custom Renderer来设置它,比如:@ 987654331@ ,否则如果 Xamarin.IOS 用作答案。如果不清楚,您可以显示有关自定义文本文件的代码。
    • @Kikanye 如果需要,这里是Cusotm Renderer document 供参考。
    • 这是一个Xamarin.Forms 项目,但这仅适用于项目的Xamarin.IOS 部分,因为对于Xamarin.Android 版本来说这很容易做到。我寻求解释(以 cmets 的形式)的主要原因更多是为了让我理解代码并能够在需要时对其进行更改,而不仅仅是复制和粘贴。如果你能解释Delegate 在这种情况下你已经使用它的功能,并在算法中添加一些 cmets,那就太棒了,我将测试运行代码,如果它有效,我可以将此答案标记为正确。 :)
    • 我正在使用一个效果来实现这个,我希望我可以使用你的代码作为参考来实现这个效果。我已经为我的问题添加了代码。此外,如果您认为使用 Delegate 仍然是最好的方法,您能解释一下使用效果器如何工作吗?谢谢,期待您的回答:)
    【解决方案2】:

    经过一番努力,我终于能够完成这项工作。非常感谢@JuniorJiang-MSFT 引导我朝着正确的方向前进。我的实现效果的文件的代码如下所示:

    using System;
    using UIKit;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    using Foundation;
    
    [assembly: ResolutionGroupName("Xamarin")]
    [assembly: ExportEffect(typeof(MyApp.iOS.CustomRenderers.ShowHidePasswordEffect), "ShowHidePasswordEffect")]
    namespace MyApp.iOS.CustomRenderers
    {
        public class ShowHidePasswordEffect : PlatformEffect
        {
            //The actual text entered by the user.
            private NSMutableString SecureText;
            private NSTimer timer;
            //The last character(s) entered by the user.
            private NSString LastText;
            private UITextView UiTextViewForControl;
            protected override void OnAttached()
            {
                SecureText = new NSMutableString();
                ConfigureControl();
            }
    
            protected override void OnDetached()
            {
                //do nothing
            }
    
    
            private void ConfigureControl()
            {
                if (Control != null)
                { 
                    if (Control is UITextView) {
                        UITextView vUpdatedEntry = (UITextView)Control;
                        this.UiTextViewForControl = vUpdatedEntry;
    
                        var buttonRect = UIButton.FromType(UIButtonType.Custom);
                        buttonRect.SetImage(UIImage.FromBundle("show_black_24"), UIControlState.Normal);
                        buttonRect.TouchUpInside += (object sender, EventArgs e1) => {
                            if (vUpdatedEntry.SecureTextEntry)
                            {
                                vUpdatedEntry.SecureTextEntry = false;
                                buttonRect.SetImage(UIImage.FromBundle("hide_black_24"), UIControlState.Normal);
                            }
                            else
                            {
                                vUpdatedEntry.SecureTextEntry = true;
                                buttonRect.SetImage(UIImage.FromBundle("show_black_24"), UIControlState.Normal);
                            }
                            // Change the text based on whether password is to be hidden or visible.
                            HandleSecureEntryChange(vUpdatedEntry);
                        };
    
                        vUpdatedEntry.ShouldChangeText += (textView, range, toReplaceText) => {
                            if ("\n" == toReplaceText)
                            {   //Drop the keyboard if 'Enter/Return' is pressed
                                vUpdatedEntry.ResignFirstResponder();
                                return false;
                            }
                            //Get the text that was entered, store that in LastText. Return true, so that Changed is called.
                            LastText = new NSString(toReplaceText);
                            return true;
                        };
                        // Add eventHandler for Changed.
                        vUpdatedEntry.Changed += HandleTextChange;
    
                        buttonRect.ContentMode = UIViewContentMode.ScaleToFill;
    
                        UIView paddingViewRight = new UIView(new System.Drawing.RectangleF(0.0f, 0.0f, 30.0f, 18.0f));
                        paddingViewRight.AddSubview(buttonRect);
    
                        buttonRect.TranslatesAutoresizingMaskIntoConstraints = false;
                        buttonRect.CenterYAnchor.ConstraintEqualTo(paddingViewRight.CenterYAnchor).Active = true;
    
                        vUpdatedEntry.AddSubview(paddingViewRight);
    
                        paddingViewRight.TranslatesAutoresizingMaskIntoConstraints = false;
                        paddingViewRight.TrailingAnchor.ConstraintEqualTo(vUpdatedEntry.LayoutMarginsGuide.TrailingAnchor, 8.0f).Active = true;
                        paddingViewRight.HeightAnchor.ConstraintEqualTo(vUpdatedEntry.HeightAnchor).Active = true;
                        paddingViewRight.BottomAnchor.ConstraintEqualTo(vUpdatedEntry.LayoutMarginsGuide.BottomAnchor).Active = true;
                        paddingViewRight.TopAnchor.ConstraintEqualTo(vUpdatedEntry.LayoutMarginsGuide.TopAnchor, -8.0f).Active = true;
                        paddingViewRight.WidthAnchor.ConstraintEqualTo(buttonRect.WidthAnchor, 1.0f, 3.0f).Active = true;
                        vUpdatedEntry.TextContainerInset = new UIEdgeInsets(8.0f, 0.0f, 8.0f, paddingViewRight.Frame.Width + 5.0f);
    
                        Control.Layer.CornerRadius = 4;
                        Control.Layer.BorderColor = new CoreGraphics.CGColor(255, 255, 255);
                        Control.Layer.MasksToBounds = true;
                        vUpdatedEntry.TextAlignment = UITextAlignment.Left;
                    }
                }
            }
    
            private void HandleTextChange(object sender, EventArgs e)
            {
                UITextView customEditor = (UITextView)sender;
                if (customEditor.SecureTextEntry)
                {
                    string text = customEditor.Text;
                    if (text.Length > 0)
                    {
                        //If LastText is empty, that means deletion occured.
                        if ("" == this.LastText)
                        {   //Delete the last character from the actual text
                            this.SecureText.DeleteCharacters(new NSRange(this.SecureText.Length - 1, 1));
                            if (null != timer)
                            {
                                timer.Invalidate();
                            }
                            return;
                        }
                        // If LastText is not empty, that means (a) new character(s) was/were entered.
                        int lastTextLength = ((this.LastText.ToString()).Length); //Number of characters in LastText.
                        if (lastTextLength > 1)
                        {   // If more than one character was entered (usually by pasting)
                            int NewTextStartIndex = (text.Length - lastTextLength);
                            string TempNewCharacters = text.Substring(NewTextStartIndex);
                            // Get all the characters except the last character and add them to the secureText
                            NSString NewCharacters = new NSString(TempNewCharacters.Substring(0, TempNewCharacters.Length - 1));
                            SecureText.Append(NewCharacters);
    
                        }
                        // Get and add the last character
                        NSString LastCharacter = new NSString(text.Substring(text.Length - 1));
                        this.SecureText.Append(LastCharacter);
                        // Change all characters to '●' except the last character
                        NSMutableString temp = new NSMutableString();
                        for (int i = 0; i < this.SecureText.Length - 1; i++)
                        {
                            temp.Append(new NSString("●"));
    
                        }
                        // Add the last character as a plain text to the temp and set that into the editor.
                        temp.Append(new NSString(this.SecureText.ToString().Substring(this.SecureText.ToString().Length - 1)));
                        customEditor.Text = temp;
    
                        if (null != this.timer)
                        {
                            this.timer.Invalidate();
                        }
                        // Wait two seconds before changing the last character to '●'.
                        this.timer = NSTimer.CreateScheduledTimer(2, ChangeToHidden);
                    }
                    else
                    {
                        this.SecureText = new NSMutableString();
                    }
                }
                else
                {
                    // If the password is plain text, then initialize it to the secureText string.
                    SecureText = new NSMutableString();
                    SecureText.SetString(new NSString(customEditor.Text));
                    if (null != timer)
                    {
                        this.timer.Invalidate();
                    }
                }
                CustomEditor ActualCustomEditor = (CustomEditor)Element;
                ActualCustomEditor.PlainText = SecureText.ToString();
            }
    
            private void ChangeToHidden(object sender)
            {
                // Change all the text entered to '●'
                UITextView customEditor = this.UiTextViewForControl;
                this.timer.Invalidate();
                NSMutableString temp = MakeHiddenText();
                customEditor.Text = temp;
            }
    
            private void HandleSecureEntryChange(object sender)
            {
                if (this.SecureText.Length > 0)
                {   // If SecureTextEntry is true, hide text, else make plain text and clean the timer.
                    UITextView customEditor = (UITextView)sender;
                    if (customEditor.SecureTextEntry)
                    {
                        NSMutableString temp = MakeHiddenText();
                        customEditor.Text = temp;
                    }
                    else
                    {
                        customEditor.Text = this.SecureText;
                        if (null != timer)
                        {
                            this.timer.Invalidate();
                        }
                    }
                }
            }
    
            private NSMutableString MakeHiddenText()
            //Make string of '●''s for the text and return
            {
                NSMutableString temp = new NSMutableString();
                for (int i = 0; i < this.SecureText.Length; i++)
                {
                    temp.Append(new NSString("●"));
                }
                return temp;
            }
    
        }
    }
    

    我的 CustomRenderer 的代码如下所示:

    using MyApp.CustomControls;
    using MyApp.iOS.CustomRenderers;
    using UIKit;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(CustomEditor), typeof(CustomEditorRenderer))]
    namespace MyApp.iOS.CustomRenderers
    {
        public class CustomEditorRenderer: EditorRenderer
        {
            protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
            {
                base.OnElementChanged(e);
                if (Control != null && Element != null) {
                    var element = (CustomEditor)this.Element;
                    if (element.IsPassword) {
                        UITextView uiTextView = (UITextView)Control;
                        uiTextView.SecureTextEntry = true;
                    }
                }
            }
        }
    }
    

    在子类Editor 的文件中,我添加了一个属性PlainText,它将用于在隐藏时获取实际文本。在我的 XAML 中,我将此属性绑定到应该保存输入到控件中的文本的变量。下面是我CustomEditor的相关代码。

    using System;
    using System.Collections.Generic;
    using System.Text;
    using Xamarin.Forms;
    
    namespace MyApp.CustomControls
    {
        public class CustomEditor : Editor
        {
            public static readonly BindableProperty PlainTextProperty =
                BindableProperty.Create(nameof(PlainText),
                    typeof(string),
                    typeof(CustomEditor),
                    String.Empty,
                    defaultBindingMode:BindingMode.TwoWay,
                    propertyChanged:OnPlainTextChanged);
    
            public string PlainText {
                get { return (string)GetValue(PlainTextProperty); }
                set { SetValue(PlainTextProperty, value); }
            }
    
            private static void OnPlainTextChanged(BindableObject bindable, object oldValue, object newValue)
            {
                var control = (CustomEditor)bindable;
                if (newValue != null)
                {
                    control.PlainText = newValue.ToString();
                }
            }
        }
    }
    

    注意:必须将defaultBindingMode 指定为TwoWay,并为propertyChanged 指定一个方法才能使其工作。

    【讨论】:

      【解决方案3】:

      您的问题分为两部分。

      1. 您可能可以通过子类化 UITextView 来防止复制和粘贴内容,如下所示:
      public class ProtectedTextView : UITextView
      {
          public override bool CanPerform(Selector action, NSObject withSender)
          {
              if (action == new Selector("paste:") || (action == new Selector("copy:")))
                  return false;
              else
                  return base.CanPerform(action, withSender);
          }
      }
      
      1. 对于显示/隐藏,您正在查看的内容相当于:
      textView.ShouldChangeText = (textField, range, replacementString) =>
      {
           string text = textView.Text;
           var result = text.Substring(0, (int) range.Location) + replacementString + text.Substring((int) range.Location + (int) range.Length);
           textView.Text = result;
           return false;
      };
      

      我还没有测试代码是否可以工作,所以如果您有任何问题,请告诉我。

      【讨论】:

      • 您好,感谢您的回复。为了防止从UITextView 复制,我说将SecureTextEntry 属性设置为true 可以处理这个问题,所以我不需要实现它。我还尝试使用您为答案的 2 部分提供的代码,但它不起作用,有什么调整或想法吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-10
      • 2018-08-13
      • 1970-01-01
      • 2021-02-26
      • 2017-11-27
      • 1970-01-01
      相关资源
      最近更新 更多