migueldeicaza / MonoTouch.Dialog

Tools to simplify creating dialogs with the user using MonoTouch
MIT License
430 stars 211 forks source link

Wish: Pickers as InputView, instead of a new pushed view with single control #79

Open timahrentlov opened 12 years ago

timahrentlov commented 12 years ago

I've been experimenting with creating data-entry form using dialogs. Many aspects are covered by the current version of Dialogs. And many thanks to Miquel for making our lives easier. But stuff like DatePickers on ipads does not seem that useful right now.

When choosing a DataTimeElement, it pushes an entire new view instead of using the InputView trick like this one seems to do: https://gist.github.com/965257

migueldeicaza commented 12 years ago

Can you paste a screenshot of what is it that you see and what it is that you want?

timahrentlov commented 12 years ago

Sure,

I get something like this:

Have this

when what I really want is something like this:

Want this

It would be really nice if the existing Date/Time Element's could be set up to use the InputView model instead.

Although not finished and with issues (got to stop working for today), I wipped up something along these lines:

[Preserve(AllMembers=true)] public class DatePickerElement : Element { static NSString key = new NSString ("datepickerelement"); public DateTime dateTime; DatePickerCell _cell;

    public class DatePickerCell : UITableViewCell 
    {
        DateTime _dateTime;

        UITableViewCell cellView;
        UIDatePicker _dp = null;
        UIView _dpview = null;
        UITextField _dummyFld;
        UITextField _entryFld;

        public DatePickerCell (DateTime newdate, NSString identKey) : base (UITableViewCellStyle.Default, identKey)
        {
             // Configure your cell here: selection style, colors, properties
            _dateTime = newdate;

            // Create Date Picker
            _dp = new UIDatePicker(RectangleF.Empty); 
            _dp.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
            _dp.Date = newdate;
            _dp.Mode = UIDatePickerMode.Date;
            _dp.ValueChanged += delegate { OnValueChanged();};
            _dpview = new UIView(_dp.Bounds);
            _dpview.AddSubview(_dp);

            // Add Controls
            _dummyFld = new UITextField(Bounds);
            _dummyFld.Tag = 1;
            _dummyFld.Text = newdate.ToString();
            _dummyFld.BackgroundColor = UIColor.Clear;
            _dummyFld.ShouldBeginEditing += delegate 
            {
                _entryFld.BecomeFirstResponder();
                return false;   
            };

            _entryFld = new UITextField(Bounds); 
            _entryFld.Hidden = true;
            _entryFld.Tag = 2;
            _entryFld.InputView = _dpview;
            _entryFld.BackgroundColor = UIColor.Clear;
            _entryFld.Started += delegate 
            {
            };

            _entryFld.Ended += delegate 
            {
                _dateTime = _dp.Date;
            };

            // Update Fld views
            _dummyFld.AddSubview(_entryFld);
            cellView = new UITableViewCell();
            cellView.AddSubview(_dummyFld);
            ContentView.AddSubview(cellView);
        }

        public override void LayoutSubviews ()
        {
             base.LayoutSubviews ();
             cellView.Frame = ContentView.Bounds;
             cellView.SetNeedsDisplay ();
        }

        // Called by our client code when we get new data.
        public void UpdateCell(DateTime newdate)
        {
            _dateTime = newdate;
        }

        public void OnValueChanged()
        {
            _entryFld.ResignFirstResponder();
        }
    }

    public DatePickerElement (DateTime newDate) : base (null)
    {
        dateTime = newDate;
    }

    public override UITableViewCell GetCell (UITableView tv)
    {
        var cell = tv.DequeueReusableCell (key) as DatePickerCell;
        if (cell == null)
            cell = new DatePickerCell(dateTime, key);
        else
           cell.UpdateCell(dateTime);

        _cell = cell;   
        return cell;
    }

    public override void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath path)
    {
        base.Selected (dvc, tableView, path);
        _cell.BecomeFirstResponder();
    }

    // Own Delegate
    public void BeforeRemove()
    {
    }
}
timahrentlov commented 12 years ago

As a general observation I believe that pickers of various sort should be prioritized. Stuff like #of: hours, months weeks, etc are sorely needed when it comes to making entry forms in a hurry. Again, with InputView as the dominant way of showing pickers, since I can always use a sub-root to implement sub-level functionality. I need these things like, yesterday. At least if productivity is a virtue :)

migueldeicaza commented 12 years ago

I am not sure what the user experience should be like if the user touches the on/off switch. I do not know how to detect that the user is messing around with another cell to hide the other cells.

Apple sort of works around that issue by limiting the content that you can click on in the Settings app. Do you have links to other apps that mix date entry with other entries that I could take a look at?

Also, do you care to explain why you added those UITextFields to your sample? I am not sure what it is that you were doing there.

timahrentlov commented 12 years ago

UI Experience

Well, one could always resort to the crude old:

EXAMPLE 1

Perhaps one could do a BecomeFirstResponder on newly tapped cells instead? Thus removing the inputview? Worst case, could you not remember the last element tapped and resign it when a new one is tapped? In this more elegant input model the update of the , say, date "text" inside the element should happen instantly while turning the picker wheel.

One thing is certain though. The current implementation does not work so even the crude model would be preferable to status quo.

UITextFields

As mentioned, I had a look at https://gist.github.com/965257 and copied much of its code-idea. I too wondered why 2 fields on top of each other was necessary in the example. But after playing with the code I could see that you needed a hidden field on top of an editable field in order to avoid manual text input. It looks like fields need to be editable and actually be able to receive input to produce an inputview, but I have not researched using inputview on other controls than UILabel (where inputview seems readonly) and UITextField.

timahrentlov commented 12 years ago

For the "crude" model I would use something like the InputAccessoryView.

timahrentlov commented 12 years ago

Or perhaps somehing like this https://github.com/sickanimations/ActionSheetPicker where actionsheets and buttons are used. After some research it looks like that modal presentation (which often is the case for forms that creates entries) and inputviews are hard to combine.

timahrentlov commented 12 years ago

After investigating many approaches, some of which gets your app rejected by Apple, I think I'm going to roll with something like this on iPad:

public class MyDatePicker
{
    UIViewController _vc;
    UIPopoverController _pop;
    DateTime _dateTime;
    UIDatePicker _dp = null;

    public MyDatePicker (Element element)
    {
        _vc = new UIViewController();
        _pop = new UIPopoverController(_vc);
        _pop.DidDismiss += delegate { OnDone();};

        _vc.View.Frame = new RectangleF(0,0,340,300);

        // Create Date Picker
        _dp = new UIDatePicker(new RectangleF(0,0,320,300)); 
        _dp.Date = DateTime.Now;
        _dp.Mode = UIDatePickerMode.Date;
        _dp.ValueChanged += delegate { OnValueChanged();};

        // Add to view
        _vc.View.AddSubview(_dp);

        // Present
        var activecell = element.GetActiveCell();
        _pop.SetPopoverContentSize(new SizeF(_dp.Bounds.Width,_dp.Bounds.Height), true);
        _pop.PresentFromRect(activecell.Frame,activecell.Superview,UIPopoverArrowDirection.Left,true);

    }

    public void OnDone()
    {

    }

    public void OnValueChanged()
    {}

}
timahrentlov commented 12 years ago

To all that currently have a need for a quick ipad alternative, here are some classes that I use until Miquel incorporates something fancier :)

public class DatePickerElement : StringElement 
{
    DatePickerHelper _currdatepickerhelper;
    DateTime _currdate;
    OnPickerEvent _ope;

    [Preserve(AllMembers=true)]
    public class DatePickerHelper
    {
        UIViewController _vc;
        UIPopoverController _pop;
        UIDatePicker _dp = null;
        DatePickerElement _element;

        public DatePickerHelper (DatePickerElement element)
        {
            _vc = new UIViewController();
            _pop = new UIPopoverController(_vc);
            _pop.DidDismiss += delegate { OnDone();};

            _vc.View.Frame = new RectangleF(0,0,340,300);

            // Create Date Picker
            _dp = new UIDatePicker(new RectangleF(0,0,320,300)); 
            _dp.Date = element.GetDate ();
            _dp.Mode = UIDatePickerMode.Date;
            _dp.ValueChanged += delegate { OnValueChanged();};

            // Add to view
            _vc.View.AddSubview(_dp);

            // Present
            var activecell = element.GetActiveCell();
            _pop.SetPopoverContentSize(new SizeF(_dp.Bounds.Width,_dp.Bounds.Height), true);
            _pop.PresentFromRect(activecell.Frame,activecell.Superview,UIPopoverArrowDirection.Down,true);

            // Remember element
            _element = element;
        }

        public void OnDone()
        {
            _element.ResetPicker(); 
        }

        public void OnValueChanged()
        {
            _element.SetDate(_dp.Date);
        }

    }

    public DatePickerElement (string initcaption, DateTime initdate, OnPickerEvent onpickerevent) : base ("")
    {
        Caption = initcaption;
        SetDate (initdate);
        _ope = onpickerevent;
    }

    public override void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath indexPath)
    {
        base.Selected (dvc, tableView, indexPath);
        _currdatepickerhelper = new DatePickerHelper(this);
    }
    public void ResetPicker()
    {
        _currdatepickerhelper = null;
        if (_ope != null)
        _ope();
    }

    public void SetDate(DateTime newdate)
    {
        _currdate = newdate.ToLocalTime(); 
        Value = _currdate.ToLongDateString();

        if (GetContainerTableView() != null)
            GetContainerTableView().ReloadData();
    }

    public DateTime GetDate()
    {
        return _currdate;
    }   

}

public class TimePickerElement : StringElement 
{
    TimePickerHelper _currtimepickerhelper;
    DateTime _currtime;
    OnPickerEvent _ope;

    [Preserve(AllMembers=true)]
    public class TimePickerHelper
    {
        UIViewController _vc;
        UIPopoverController _pop;
        UIDatePicker _dp = null;
        TimePickerElement _element;

        public TimePickerHelper (TimePickerElement element)
        {
            _vc = new UIViewController();
            _pop = new UIPopoverController(_vc);
            _pop.DidDismiss += delegate { OnDone();};

            _vc.View.Frame = new RectangleF(0,0,150,300);

            // Create Date Picker
            _dp = new UIDatePicker(new RectangleF(0,0,150,300)); 
            _dp.Date = element.GetTime ();
            _dp.Mode = UIDatePickerMode.Time;
            _dp.ValueChanged += delegate { OnValueChanged();};

            // Add to view
            _vc.View.AddSubview(_dp);

            // Present
            var activecell = element.GetActiveCell();
            _pop.SetPopoverContentSize(new SizeF(_dp.Bounds.Width,_dp.Bounds.Height), true);
            _pop.PresentFromRect(activecell.Frame,activecell.Superview,UIPopoverArrowDirection.Down,true);

            // Remember element
            _element = element;
        }

        public void OnDone()
        {
            _element.ResetPicker(); 
        }

        public void OnValueChanged()
        {
            _element.SetTime(_dp.Date);
        }

    }

    public TimePickerElement (string initcaption, DateTime inittime, OnPickerEvent onpickerevent) : base ("")
    {
        Caption = initcaption;
        SetTime (inittime);
        _ope = onpickerevent;
    }

    public override void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath indexPath)
    {
        base.Selected (dvc, tableView, indexPath);
        _currtimepickerhelper = new TimePickerHelper(this);
    }

    public void ResetPicker()
    {
        _currtimepickerhelper = null;
        if (_ope != null)
        _ope();
    }

    public void SetTime(DateTime newtime)
    {
        _currtime = newtime.ToLocalTime();;
        Value = _currtime.ToShortTimeString();
        if (GetContainerTableView() != null)
            GetContainerTableView().ReloadData();
    }

    public DateTime GetTime()
    {
        return _currtime;
    }   

}

public class DurationPickerElement : StringElement 
{
    DurationPickerHelper _currdurationpickerhelper;
    double _currduration;
    OnPickerEvent _ope;

    [Preserve(AllMembers=true)]
    public class DurationPickerHelper
    {
        UIViewController _vc;
        UIPopoverController _pop;
        UIDatePicker _dp = null;
        DurationPickerElement _element;

        public DurationPickerHelper (DurationPickerElement element)
        {
            _vc = new UIViewController();
            _pop = new UIPopoverController(_vc);
            _pop.DidDismiss += delegate { OnDone();};

            _vc.View.Frame = new RectangleF(0,0,340,300);

            // Create Date Picker
            _dp = new UIDatePicker(new RectangleF(0,0,320,300)); 
            _dp.Mode = UIDatePickerMode.CountDownTimer;
            _dp.MinuteInterval = 5;
            _dp.CountDownDuration = element.GetDuration();
            _dp.ValueChanged += delegate { OnValueChanged();};

            // Add to view
            _vc.View.AddSubview(_dp);

            // Present
            var activecell = element.GetActiveCell();
            _pop.SetPopoverContentSize(new SizeF(_dp.Bounds.Width,_dp.Bounds.Height), true);
            _pop.PresentFromRect(activecell.Frame,activecell.Superview,UIPopoverArrowDirection.Down,true);

            // Remember element
            _element = element;
        }

        public void OnDone()
        {
            _element.ResetPicker(); 
        }

        public void OnValueChanged()
        {
            _element.SetDuration(_dp.CountDownDuration);
        }

    }

    public DurationPickerElement (string initcaption, double initduration, OnPickerEvent onpickerevent) : base ("")
    {
        Caption = initcaption;
        SetDuration (initduration);
        _ope = onpickerevent;
    }

    public override void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath indexPath)
    {
        base.Selected (dvc, tableView, indexPath);
        _currdurationpickerhelper = new DurationPickerHelper(this);
    }

    public void ResetPicker()
    {
        _currdurationpickerhelper = null;
        if (_ope != null)
            _ope();
    }

    public void SetDuration(double newduration)
    {
        _currduration = newduration;

        if (newduration != 0.0)
        {
            var hour = (int)_currduration / 3600;
            var min = (int)(_currduration % 3600)/60;
            Value = String.Format("{0:00}:{1:00}",hour, min);
        } else
            Value = "";

        if (GetContainerTableView() != null)
            GetContainerTableView().ReloadData();
    }

    public double GetDuration()
    {
        return _currduration;
    }   

}
digitalml commented 12 years ago

null - will your above code work for the iPhone as well? If so, can you please point me to the definition for OnPickerEvent. I can't seem to find it anywhere. Thanks!

timahrentlov commented 12 years ago

public delegate void OnPickerEvent(); The code is iPad only.

johnsimons commented 12 years ago

I've submit a pull request to get started on this: https://github.com/migueldeicaza/MonoTouch.Dialog/pull/107

And here is it with date pickers: https://github.com/johnsimons/MonoTouch.Dialog/tree/AllPickers

timahrentlov commented 12 years ago

Nice job, John. Hopefully it gets included soon.